Este módulo profesional amplía la formación necesaria para desempeñar la función de desarrollador de aplicaciones multiplataforma, en la parte de Acceso a Datos. La función de desarrollador de aplicaciones multiplataforma incluye aspectos como:
Desarrollo de aplicaciones de gestión de ficheros y directorios.
Desarrollo de aplicaciones de acceso a bases de datos relacionales.
Desarrollo de aplicaciones que hagan uso de bases de datos orientadas a objetos.
Desarrollo de aplicaciones de acceso a bases de datos XML y bases de datos NoSQL, como MongoDB.
Desarrollo de componentes de acceso a datos y su integración en aplicaciones.
Las actividades profesionales asociadas a esta función se aplican en el desarrollo de software de gestión multiplataforma con acceso a bases de datos utilizando lenguajes, bibliotecas y herramientas adecuadas las especificaciones.
Todas las empresas en las que los alumnos pueden trabajar al finalizar el ciclo utilizarán alguna tecnología de persistencia de su información. El alumno aprenderá a trabajar con las diferentes tecnologías de persistencia de los datos utilizadas más habitualmente, de forma que pueda adaptarse al entorno existente en el centro de trabajo una vez finalice el ciclo.
Comenzamos
En primer lugar, os quería desear mucho ánimo a todas/os, pues esta materia es la continuación natural de la materia de Programación que habéis cursado en primero, pero centrándonos en la parte del modelo de acceso a los datos. Es el complemento principal de la materia de Desenvolvemento de Interfaces y Programación Multimedia e Dispositivos Móbiles, en la que se trabaja la parte de la vista y el controlador.
Como habéis podido comprobar en la materia de primero, a programar se aprende programando, como con cualquier otra actividad que requiera destreza, como tocar un instrumento, cocinar, un deporte, etc. Seguro que más de un/a sabe a qué me refiero. Por ello intentaremos minimizar la carga teórica para centrarnos en las actividades prácticas y las tareas, eso no implica que no puedan caer cuestiones teóricas sobre cuestiones prácticas (patrones de diseño, etc.).
Realizaremos programas a diario y, si no podemos, organicemos nuestro tiempo para poder dedicarle a programar unas horas por semana. Como en primer curso (en el que también habéis trabajado con Kodlin) trabajaremos principalmente con el lenguaje de programación Java, para mí, el lenguaje más completo, flexible y útil desde un punto de vista didáctico, aunque veremos **también ejemplos y ejercicios con Kotlin, especialmente para acceso a datos desde Android:
En principio, nos centraremos en la parte de acceso a ficheros/bases de datos, proporcionando la parte de la vista, pero, a medida que avancéis en materias como diseño de interfaces, completaréis la parte de la vista para hacer programas completos.
A lo largo del curso veremos tecnologías de acceso a datos como:
Si nos da tiempo, también veremos otras bases de datos NoSQL como la mencionada Cassandra y bases de datos orientadas a objetos como BaseX (sólo si llegamos a tiempo).
En definitiva, tecnologías actuales y demandadas en el mercado laboral para acceso a la información.
Consultad la información que aparece en el curso de tutoría de DAM:
Durante estos primeros días, haremos un pequeño repaso de Java y empezaremos la primera unidad, para que poco a poco os vayáis familiarizando con el entorno y la plataforma, es importante que actualicéis el perfil, y leáis esta guía completa (lo estáis haciendo), así como los documentos legales que aparecen en el curso de tutoría. Poned una foto real (perdón por no haberlo hecho, o casi).
Os adelanto, sin intención de dar miedo, al contrario, con ánimo de prevenir y que trabajéis desde ahora, porque, aunque con los conocimientos previos de programación os resultará sencillo, requiere práctica constante (sobre todo en clase) y enfrentarse a la materia con buen ánimo.
Unidades didácticas
El curso de Acceso a Datos de DAM consta de varias unidades didácticas. Cuando se comparta la Programación del Curso podréis ver los detalles de cada una de ellas. Por el momento se trata sólo de un primer borrador, que será completado al detalle en cada unidad didáctica.
A modo de adelanto, se indica la temporización aproximada de dichas unidades, teniendo en cuenta que son 9 sesiones por semana.
Primera evaluación
Se impartirán las dos primeras unidades y parte de la tercera unidad de herramientas ORM.
UD 1. Acceso a ficheros, flujos, serialización de objetos, ficheros JSON y XML. (UD. 1)
XML con Java: procesadores DOM y SAX, las clases específicas para el tratamiento de la información contenida en un fichero XML, las clases específicas para la vinculación de objetos, las bibliotecas para conversión de documentos XML a otros formatos.
Gestión de información almacenada en ficheros, flujos, haciendo especial hincapié en los formatos JSON y algo de XML mediante aplicaciones informáticas escritas en Java.
a) Gestión de flujos, ficheros secuenciales, Acceso Directo y Directorios: desarrollo de aplicaciones que gestionan información almacenada en ficheros secuenciales, de acceso directo y en el sistema de directorios. En ella se aprenderá a identificar y utilizar las clases específicas para operar con cada tipo de fichero y con el sistema de directorios y a manejar las excepciones para el tratamiento de los posibles errores.
b) Gestión de ficheros JSON y, en menor medida, XML: desarrollo de aplicaciones que gestionan información almacenada en ficheros JSON:
También veremos algo de XML, y prenderemos a utilizar los procesadores DOM y SAX, las clases específicas para el tratamiento de la información contenida en un fichero XML, las clases específicas para la vinculación de objetos, las bibliotecas para conversión de documentos XML a otros formatos y a manejar las excepciones para el tratamiento de los posibles errores.
UD 2. Acceso a BD locales y remotas relacionales. Creación de una interfaz web sencilla (Vaadin y/o Thymeleaf)
Gestión de información almacenada en bases de datos relacionales por medio de aplicaciones informáticas escritas en Java con JDBC.
Para facilitar el trabajo de aplicaciones sencillas, existen muchos SGBD relacionales orientados a archivo (embebidos) opensource como H2, SQLite, HSQL, tinySQL, smallSQL o comerciales:
Pondremos especial interés en PostgreSQL, SQLite y H2, que son los más empleados en aplicaciones de escritorio y móviles.
Veremos patrones de diseño DAO y DTO, y cómo realizar operaciones CRUD (Create, Read, Update, Delete) sobre la base de datos.
Creación de una interfaz web sencilla, reflejado en el Proyecto curricular de Centro, probablemente con el framework Vaadinhttps://vaadin.com/ o Thymeleafhttps://www.thymeleaf.org/, que veremos también con Spring Boot.
Aplicaciones que gestionan información almacenada en bases de datos relacionales. En ella se aprenderá a establecer conexiones con gestores de bases de datos relacionales embebidos e independientes utilizando conectores, a realizar operaciones de descripción, consulta y modificación de los datos contenidos en la base de datos, la extracción de los datos para realizar de forma adecuada las operaciones anteriores, a gestionar las transacciones y a manejar las excepciones para el tratamiento de los posibles errores
UD 3. Herramientas de mapeo objeto-relacional (ORM).
Parte de esta unidad se realizará en la segunda evaluación
Desarrollo de aplicaciones que gestionan información almacenada en base de datos relacionales utilizando herramientas mapeo objeto relacional (ORM), con JPA sobre Hibernate/EclipseLink y Spring Data, que traduzca la lógica de los objetos a la lógica relacional para su manipulación más sencilla.
En ella se aprenderá a instalar y configurar la herramienta ORM, a definir los ficheros de mapeo o, mejor, mediante anotaciones y las clases persistentes, a realizar el mapeo objeto relacional, a establecer sesiones, a cargar, almacenar y modificar los objetos persistentes, a realizar consultas en el lenguaje JPQL y en el lenguaje propio de la herramienta ORM.
También veremos Spring Boot y Spring Data, que facilitan el acceso a datos en aplicaciones Java, y que se integran perfectamente con JPA y Hibernate.
Segunda evaluación
En esta segunda evaluación se completará la tercera unidad y se impartirán las siguientes unidades. La unidad de Herramientas de mapeo objeto-relacional (ORM) se completará en esta evaluación, mientras que la unidad de creación de componentes se impartirá a lo largo del curso.
UD 4. Bases de datos no SQL. MongoDB
Características de las Bases de datos NoSQL.
Manejo de la información en bases de datos NoSQL.
Creación de aplicaciones informáticas que acceden a bases de datos NoSQL.
En ella se aprenderá a manejar y establecer conexiones con el gestor de bases de datos NoSQL, a realizar consultas y modificaciones de los datos contenidos en la base de datos. Para ello emplearemos dos estrategias: la API de bajo nivel y la API de alto nivel:
API nativa de MongoDB.
API de alto nivel de Spring Data MongoDB.
Ejemplos típicos de bases de datos NoSQL incluyen:
UD 5. Bases de datos nativas XML. Bases de datos orientadas a objeto. BD objeto-relacionales
Aplicaciones que gestionan la información almacenada en bases de datos nativas XML, como BaseX. En ella se aprenderá a manejar y establecer conexiones con el gestor de bases de datos nativas XML, a realizar consultas utilizado los lenguajes XPath y Xquery, o a modificar y eliminar los documentos XML.
Aplicaciones que gestionan información almacenada en bases de datos objeto-relacionales y orientadas a objetos. En ella se aprenderá a manejar gestores de base de datos que extienden las bases de datos relacionales añadiendo conceptos del modelo orientado a objetos y gestores de base de datos que almacenen los datos como objetos, estableciendo conexiones con estos tipos de gestores y realizando operaciones de almacenamiento, modificación y consulta de los objetos persistentes.
UD 6. Programación de componentes de acceso a datos
Programación de componentes de acceso a datos se irá estudiando a lo largo del curso, pues dichos componentes son la base de las herramientas ORM y de acceso a datos que hemos visto en las unidades anteriores.
En ella se aprenderá las bases de la programación orientada a componentes para las construcciones de aplicaciones basadas en el ensamblado de módulos reutilizables y se programarán componentes para el acceso los datos contenidos en diferentes sistemas de persistencia de datos utilizando herramientas de desarrollo de componentes. Materias como Diseño de Interfaces y Programación Multimedia y Dispositivos Móviles, así como la de Entornos de Desarrollo, serán fundamentales para completar esta unidad.
Parciales y evaluación
En cada evaluación se realizará un único examen parcial, aunque podría realizarse algún control después de cada unidad temática para realizar un seguimiento y una evaluación independiente de cada unidad. El examen parcial debe superarse para aprobar la evaluación correspondiente (nota mayor que 5).
Las pruebas o exámenes serán eminentemente prácticos y no se podrá disponer de material de ayuda, salvo el que el profesor o profesora considere necesario.
Cada unidad se evaluará de modo independiente, siendo necesario superar todas las unidades para aprobar la materia.
Se contempla la realización de prácticas y trabajos, al menos dos durante el curso, pero una por cada unidad, que se valorarán con un máximo de 1,5 puntos sobre la nota final.
Podrían plantearse trabajos o prácticas no obligatorias, que podrían emplearse para subir nota en caso de que fuese necesario y así lo considerase el profesor.
La nota de la evaluación está formada por el resultado del examen, las tareas obligatorias y un mínimo, referido a la participación y actitud durante el curso, con pesos de **85%, 10-15% y 0-5%, respectivamente**, Si durante la evaluación no se han realizado trabajos obligatorios el examen tendría un peso del porcentaje asociado a dicha nota. Una falta grave podría significar la suspensión de dicha evaluación.
El/los examen/es presencial de cada evaluación (2 en total) tendrá un valor de entre 8,5 y 10 puntos sobre la nota, dependiendo de si se ha realizado o no una tarea obligatoria. La puntuación restante se corresponde a la valoración de las tareas de la evaluación concreta.
Es imprescindible obtener un 5 sobre 10 en el examen para poder hacer media y aprobar. Aun así, es preciso que la media total, contando las tareas, sea igual o superior a un 5.
Aunque las notas tendrán decimales, en el boletín de la evaluación se redondearán a valores enteros más próximos a dicha nota, entre 1 y 10.
Los/as estudiantes que hayan superado las evaluaciones parciales habrán aprobado la asignatura y no se presentarán al examen final.
La nota final será la media de todas las evaluaciones, siempre que se superen las dos evaluaciones.
Aunque la evaluación de cada unidad es independiente, cada unidad podrá incluir conceptos básicos necesarios de unidades previas (flujos, ficheros, etc.)
Examen final
Aquellas personas que hayan suspendido alguna de las dos evaluaciones deberán realizar un examen final con las partes pendientes, en principio toda la evaluación suspensa, aunque podría realizarse de una unidad concreta si así se considera.
El examen final constará de varios apartados, uno por cada unidad, de carácter eminentemente práctico que contenga la materia estudiada en cada una de las unidades. Si se supera, si se obtiene más de un 5, se aprobará la materia.
Se mantendrán las notas de las prácticas obligatorias entregadas a la hora de hacer la media del curso, que sí contabilizan en la nota final con el mismo peso que en las evaluaciones, además de las notas de las evaluaciones aprobadas. Se calculará como media de las notas de cada evaluación con decimales, para ser redondeada a la hora de poner la nota final, aproximada al entero más próximo a la nota media.
Los ejercicios de cada unidad que sean autoevaluables o boletines de clase no cuentan para la nota, aun así, podrían ser tenidos en cuenta (no la nota, sí el hecho de participar), junto con la participación, actitud, etc. para decantar alguna nota que pueda no ajustarse a los baremos estrictamente legales y establecidos.
Como he dicho, ¡mucho ánimo!, estoy convencido de que los resultados van a ser muy positivos, sólo requiere constancia y trabajo constante. A programar se aprende programando, como con cualquier otra actividad que requiera destreza, como tocar un instrumento.
El formato de archivo Java™ Archive (JAR) te permite agrupar múltiples archivos en un solo archivo de archivo.
Un archivo JAR contiene los archivos de clase y los recursos auxiliares asociados aplicaciones.
Beneficios:
Seguridad: se puede firmar digitalmente el contenido de un archivo JAR.
Tiempo de descarga reducido: cuando se trabaja con descarga de Internet pueden descargarse a un navegador
en una sola transacción HTTP sin la necesidad de abrir una nueva conexión para cada archivo.
Formato comprimido: es un formato comprimido disminuyendo el espacio para su distribución y despliegue.
Empaquetado para bibliotecas Java: se pueden agregar bibliotecas, así como crear tus propias bibliotecas para distribuilas.
Sellado de paquetes: puede sellarse un paquete dentro de un archivo JAR para que todas las clases definidas en ese paquete estén dentro del
archivo JAR.
Versionado de paquetes: puede contener datos sobre los archivos que contiene, como información de proveedor y versión.
Portabilidad: es una parte estándar de la API principal de la plataforma Java.
1. Sintaxis Archivos JAR
Los archivos JAR están empaquetados con el formato de archivo ZIP.
Para realizar tareas básicas con archivos JAR se emplea la herramienta jar del JDK, que se invoca mediante el comando jar.
Sintaxis general:
jar {ctxui}[vfm0Me][jar-file][manifest-file][entry-point][-C dir] files ...
Operaciones principales con Archivos JAR:
Operación
Comando
Crear un archivo JAR
jar cf jar-file input-file(s)
Ver el contenido de un archivo JAR
jar tf jar-file
Extraer el contenido de un archivo JAR
jar xf jar-file
Extraer archivos específicos de un JAR
jar xf jar-file archived-file(s)
Ejecutar una aplicación empaquetada como JAR (requiere el encabezado en el archivo Manifiest con Main-Class)
java -jar app.jar
Invocar un applet empaquetado como JAR (desprobado)
El formato básico del comando para crear un archivo JAR es:
jar cf nombre-archivo-jar archivos(s)
Las opciones y argumentos utilizados en este comando son:
La opción c indica se cree un archivo JAR.
La opción f indica que la salida debe ir a un archivo en lugar de a stdout (salida por pantalla).
nombre-archivo-jar es el nombre del archivo JAR resultante. Puede usarse cualquier nombre de archivo para un archivo JAR. Por convenio, los nombres de archivo JAR se les da una extensión .jar, aunque esto no es obligatorio.
El argumento archivos(s) es una lista separada por espacios de uno o más archivos que deseas incluir en tu archivo JAR. El argumento archivo(s) puede contener el símbolo de comodín *. Si alguno de los “archivos de entrada” son directorios, el contenido de esos directorios se agrega al archivo JAR de forma recursiva.
Las opciones c y f pueden aparecer en cualquier orden, pero no debe haber ningún espacio entre ellas.
Este comando generará un archivo JAR comprimido y lo colocará en el directorio actual.
El comando también generará un archivo MANIFEST.MF predeterminado para el archivo JAR.
El archivo de META-INF/MANIFEST.MF predeterminado contiene lo siguiente (depende de la versión de Java):
Si se le indica el nombre de la clase principal, el archivo MANIFEST.MF predeterminado se actualizará para contener la entrada Main-Class:
Main-Class: nombreClasePrincipal
Nota:
Los metadatos en el archivo JAR, como los nombres de entrada, comentarios y contenido del manifiesto, deben codificarse en UTF8.
Puedes agregar cualquiera de estas opciones adicionales a las opciones cf del comando básico:
Opciones de la orden jar
Opción
Descripción
v
Produce una salida detallada (verbose) en la creación del JAR (nombre de cada archivo a medida que se añade al archivo JAR).
0 (cero)
Indica que el archivo JAR NO se comprima.
M
Indica que no se debe generar el archivo de manifiesto predeterminado (MANIFEST.MF).
m
incluir información del manifiesto desde un archivo de manifiesto existente: jar cmf jar-file manifest-existente input-file(s).
-C
Para cambiar de directorio durante la ejecución del comando.
Advertencia: El MANIFEST.MF debe terminar con una nueva línea o retorno de carro.
La última línea no se analizará correctamente si no termina con una nueva línea o retorno de carro.
3. Visualización del contenido de un JAR
El formato básico de la orden para ver el contenido de un archivo JAR es:
jar tf jar-file
La opción t indica que deseas ver la tabla de contenidos del archivo JAR.
La opción f indica que el archivo JAR cuyo contenido se va a ver.
El argumento jar-file es la ruta y el nombre del archivo JAR cuyo contenido deseas ver.
Las opciones t y f pueden aparecer en cualquier orden, pero no debe haber ningún espacio entre ellas.
Este comando mostrará la tabla de contenidos del archivo JAR por pantalla.
Se puede añadir la opción detallada, v, para obtener información adicional sobre tamaños de archivo y fechas de
última modificación en la salida.
4. Archivos JAR como Aplicaciones
Es posible ejecutar aplicaciones empaquetadas en archivos JAR con java:
java -jar archivo-jar
Solo se puede indicar un archivo JAR, que debe contener todo el código específico de la aplicación.
Lo más importante es que el archivo Jar debe tener información de la clase con el main, que es el punto de entrada de la aplicación.
Para indicar qué clase es el punto de entrada de la aplicación, debe añadirse Main-Class al manifiesto del archivo JAR.
El encabezado tiene la forma:
Main-Class: paquete.ClasePrincipal
El valor del encabezado, paquete.ClasePrincipal, es el nombre de la clase que es el punto de entrada de la aplicación.
Nota: El atributo Main-Class es opcional en el manifiesto de un archivo JAR. Si no se especifica, el archivo JAR no se puede ejecutar directamente desde la línea de comandos.
Cuando se indica Main-Class en el archivo MANIFEST.MF, puedes ejecutar la aplicación desde la línea de órdenes:
java -jar app.jar
Para ejecutar la aplicación desde el archivo JAR que está en otro directorio, debes especificar la ruta de ese directorio:
java -jar path/app.jar
02 Creación de un JAR ejecutable con Java con Apache Maven
Este documento explicaremos los distintos modos de empaquetar un proyecto Maven en un archivo Jar ejecutable.
Cuando creamos un archivo jar, generalmente queremos ejecutarlo fácilmente, sin utilizar el IDE. Muchas veces nos vemos sorprendidos al comprobar que el archivo de manifiesto no incorpora referencias al main o no so incluyen las bibliotecas.
Veremos varias configuraciones y pros/contras de cada uno de estos enfoques para crear un JAR ejecutable.
En este artículo, describimos muchas formas de crear un jar ejecutable con varios complementos de Maven.
2. Configuración manual
Este modo de hacerlo nos da flexiblidad, pues sólo se requiere de un proyecto maven y añadir los elementos necesarios al fichero de configuración de maven, pom.xml.
No necesitamos ninguna dependencia adicional para crear un archivo jar ejecutable. sólo necesitamos crear un proyecto Java Maven y tener al menos una clase con el método main(…), la entrada al programa.
En nuestro ejemplo, creamos una clase Java llamada AppExemplo.
También debemos asegurarnos de que nuestro pom.xml contenga estos elementos:
Lo más importante aquí es el tipo de empaquetamiento, para crear un jar ejecutable el packaging tipo de jar.
Ahora podemos comenzar a usar las diversas soluciones.
2.1. Configuración del archivo pom.xml
Añadir bibliotecas con las dependencias
Comencemos con un enfoque manual con la ayuda del maven-dependency-plugin y copy-dependencies, que copia las dependencias del proyecto desde el repositorio a una ubicación definida, en este caso el directorio libs:
Primero, especificamos la meta copy-dependencies, que le dice a Maven que copie estas dependencias en el directorio de salida especificado. En nuestro caso, crearemos una carpeta llamada libs dentro del directorio de despliegue del proyecto (que suele llamarse target).
Con esta configuración de Maven se añaden las rutas a las bibliotecas del proyecto dentro de la carpeta libs/ y se indica qué clase tiene el main.
Consejo
El nombre de la clase con el main tiene que estar completamente calificado, incluytendo el nombre del paquete.
Las ventajas y desventajas de este enfoque son:
Ventajas: Proceso transparente, donde podemos especificar cada paso
Inconvenientes: se trata de un proceso manual, en el que las dependencias están fuera del jar final, lo que significa que nuestro jar ejecutable únicamente se ejecutará si la carpeta libs es accesible y visible para un jar.
2.2. Incorporación de las dependencias dentro del JAR: maven-assembly-plugin
El Plugin de ensamblado de Apache Mavenmaven-assembly-plugin permite agregar al paquete de salida del proyecto (jar en este caso), módulos, documentación del sitio y otros archivos en un único paquete ejecutable ("…permite a los desarrolladores combinar los resultados del proyecto en un único archivo distribuible que también contiene dependencias, módulos, documentación del sitio y otros archivos").
El objetivo principal (y ahora único) en el plugin de ensamblaje es el crear un archivo único, que se utiliza para crear todos los ensamblajes.
Del mismo modo que añadiendo las bibliotecas a un directorio lib, De manera similar al enfoque manual, necesitamos indicar el nombre de la clase con el main. La diferencia es que el Plugin de ensamblaje de Maven copiará automáticamente todas las dependencias necesarias dentro del mismo archivo jar.
En la parte descriptorRefs del código de configuración, se indica el nombre que se agregará al nombre del proyecto (puede cambiarse)
La salida en nuestro ejemplo se llamará core-java-jar-with-dependencies.jar: “Nota: … Tenga en cuenta que el complemento de ensamblaje le permite especificar varios descriptorRefsa la vez para producir múltiples tipos de ensamblajes en una sola invocación.”
Ventajas: las dependencias se añaden dentro de un único archivo jar, dándole portabilidad “total”.
Desventajas: no podemos reubicar las calses del proyecto. A veces, el tamaño puede aumentar considerablemente y sólo precisamos distribuir nuestras clases.
2.3. Plugin Maven Shade: maven-shade-plugin
El plugin de sombreado de Apache Maven proporciona la capacidad de empaquetar el artefacto en un uber-jar, “..proporciona la capacidad de empaquetar el artefacto en un uber-jar, incluidas sus dependencias y sombrear (es decir, cambiar el nombre) los paquetes de algunas de las dependencias.”
El archivo de configuración tiene 3 partes principales:
Necesitamos especificar la clase principal de la aplicación: com.javhoz.executable.AppExemplo
<shadedArtifactAttached> indicas las dependencias que deben ser empaquetadas en el jar.
Debe indicarse la implementación del transformador, transformer implementation=, que en el ejemplo se emplea el estándar que añade las entradas al archiv MANIFEST.
Implementaciones del transformer del plugin
Transformadores del plugin de org.apache.maven.plugins.shade.resource son:
Transformer
Descripción
ApacheLicenseResourceTransformer
Evita la duplicación de licencias
ApacheNoticeResourceTransformer
Prepara el NOTICE combinado
AppendingTransformer
Agrega contenido a un recurso
ComponentsXmlResourceTransformer
Agrega el archivo components.xml de Plexus
DontIncludeResourceTransformer
Evita la inclusión de recursos coincidentes
IncludeResourceTransformer
Agrega archivos del proyecto
ManifestResourceTransformer
Establece entradas en el MANIFEST
ServicesResourceTransformer
Fusiona los recursos META-INF/services
XmlAppendingTransformer
Agrega contenido XML a un recurso XML
El archivo de salida se llamará core-java-0.1.0-SNAPSHOT-shaded.jar, donde core-java es el nombre de nuestro proyecto seguido por la versión de snapshot y el nombre del plugin.
Ventajas: dependencias dentro del archivo jar, control avanzado del empaquetado del proyecto, con sombreado y reubicación de clases.
Deventajas: configuración compleja (especialmente si queremos usar funciones avanzadas).
2.4. Plugin One Jar Maven: onejar-maven-plugin
Otra opción, poco recomendable (como curiosidad), menos interesante y comercial para crear un jar ejecutable es el proyecto One Jar, que proporciona un loader de clases personalizado que sabe cómo cargar clases y recursos desde archivos jar dentro de un archivo, en lugar de desde archivos jar en el sistema de archivos.
Requiere dependencias.
Se debe especificar la clase principal y adjuntar todas las dependencias a la construcción, utilizando attachToBuild = true.
Se debe proporcionar el nombre de archivo de salida.
El objetivo para Maven es one-jar.
One Jar es una solución comercial que hará que las dependencias no se expandan en el sistema de archivos en tiempo de ejecución.
Ventajas: modelo de delegación limpio, permite que las clases estén en el nivel superior de One Jar, admite archivos jar externos y puede admitir bibliotecas nativas
Desventajas:: no es compatible desde 2012
2.5. Plugin Spring Boot Maven: spring-boot-maven-plugin
Otr opción interesante es el Plugin Spring Boot Maven, que permite empaquetar archivos jar o war ejecutables y ejecutar una aplicación “in situ”.
Hay dos diferencias entre el plugin Spring y los demás:
El objetivo (goal) de la ejecución se llama repackage, y el clasificador (classifier) se llama spring-boot.
NO NECESITAMOS tener una aplicación Spring Boot para usar este plugin.
Ventajas: dependencias dentro de un archivo jar, podemos ejecutarlo en cualquier ubicación accesible, control avanzado del empaquetado del proyecto, excluyendo dependencias del archivo jar, etc., empaquetado de otros tipos de archivo como war
Desventajas: añade clases innecesarias de Spring y Spring Boot, pues no requiere usar Spring Boot para emplear este plugin.
2.6. Aplicación Web ejecutable Tomcat: tomcat7-maven-plugin
Por último, si queremos hacer una aplicación web independiente que esté empaquetada dentro de un archivo jar.
Necesitamos usar un plugin diferente, diseñado para crear archivos jar ejecutables:
El goal está configurado como exec-war-only, la ruta al servidor se especifica dentro de la etiqueta de configuration, con propiedades adicionales, como finalName, charset, etc.
Para construir un jar, ejecutamos mvn package, lo que dará como resultado la creación de webapp.jar en el directorio target.
Para ejecutar la aplicación, simplemente escribimos java -jar target/webapp.jar en la consola y tratamos de probarlo especificando el localhost:8080/ en un navegador. Ya tenemos nuestra aplicación Web ejecutándose desde línea de órdenes un archivo JAR ;-)
Ventajas: tener un único archivo, fácil de implementar y ejecutar
Desventajas: el tamaño del archivo es mucho mayor, debido al empaquetado de la distribución integrada de Tomcat dentro de un archivo jar.
Implementaciones del transformer del plugin
Ten en cuenta que esta es la última versión de este plugin, que admite el servidor Tomcat7. Para evitar errores, podemos verificar que la dependencia para Servlets tenga el ámbito configurado como provided, de lo contrario, habría un conflicto en el tiempo de ejecución del jar ejecutable:
SLF4J (The Simple Logging Facade for Java) es una fachada o interfaz para varios sistemas de registro de eventos (logging) en Java. Permite a los desarrolladores cambiar de sistema de registro de eventos en tiempo de ejecución sin tener que modificar el código fuente. Para más información, visita la página oficial de SLF4J: http://www.slf4j.org/
Gson es una biblioteca Java que se utiliza para convertir objetos Java en su representación JSON. También puede ser utilizado para convertir una cadena JSON en un objeto Java equivalente. Gson es una biblioteca de código abierto desarrollada por Google. Puedes encontrar más información en la página oficial de Gson: https://github.com/google/gson
Jackson es una biblioteca Java de código abierto para convertir objetos Java en su representación JSON y viceversa. Jackson es una de las bibliotecas de serialización y deserialización JSON más populares en Java. Puedes encontrar más información en la página oficial de Jackson: https://github.com/FasterXML/jackson
Jackson Core es una biblioteca Java de código abierto para procesar JSON (Stream API). Jackson Core proporciona las clases básicas para trabajar con JSON, como JsonNode, JsonParser y JsonGenerator. Puedes encontrar más información en la página oficial de Jackson: https://github.com/FasterXML/jackson-core
JUnit es un framework open-source que se utiliza para realizar pruebas unitarias en Java. JUnit es una herramienta importante en el desarrollo de software, ya que permite a los desarrolladores probar su código de manera eficiente y asegurarse de que funciona correctamente. Puedes encontrar más información en la página oficial de JUnit: https://junit.org/junit5/
Para trabajar con bases de datos, necesitamos los drivers JDBC correspondientes.
4.1. H2
H2 es una base de datos relacional escrita en Java. Es muy rápida, de código abierto y se puede ejecutar en modo embebido o en modo servidor. Además, admite transacciones, encriptación, funciones de usuario o procedimientos almacenados. Además, puede almacenarse en memoria o en disco.
Es importante hacer notar que las incompatibilidades entre versiones diferentes de H2, por lo que se recomienda tener control sobre qué versión se está utilizando.
URL: jdbc:h2:mem:testdb (base de datos en memoria)
Driver: org.h2.Driver
URL (fichero): jdbc:h2:rutaALaBaseDatos;DATABASE_TO_UPPER=false (base de datos en fichero)
El Driver JDBC para H2 hace la conversión automática de los nombres de las tablas y columnas a mayúsculas, por lo que si queremos conservar los nombres originales, debemos añadir DATABASE_TO_UPPER=false a la URL de conexión.
4.2. SQLite JDBC Driver
SQLite es una base de datos relacional embebida, que no requiere un servidor. Es muy ligera y rápida, y se puede utilizar en aplicaciones de escritorio, móviles o en la web. Puedes encontrar más información en la página oficial de SQLite: https://www.sqlite.org/index.html
Existen varias implementaciones de SQLite en Java, pero vamos a usar Xerial SQLite JDBC Driver:
URL: jdbc:sqlite:rutaALaBaseDatos (base de datos en fichero)
Driver: org.sqlite.JDBC
Existen otras API para SQLite, como las versiones originales de androidx: https://developer.android.com/jetpack/androidx/releases/sqlite, pero dicha versión no es compatible con Java SE y se usaba antiguamente para android, antes de la aparicion de Room.
4.3. PostgreSQL JDBC Driver
PostgreSQL es un sistema de gestión de bases de datos relacional de código abierto y muy potente. Puedes encontrar más información en la página oficial de PostgreSQL: https://www.postgresql.org/
MySQL Connector/J es un controlador JDBC Tipo 4, lo que significa que es una implementación Java pura del protocolo MySQL y no depende de las bibliotecas de cliente MySQL. Como los anteriores, este controlador admite el registro automático con DriverMaganer, lo que significa que no es necesario cargar explícitamente el controlador.
HSQLDB es una base de datos relacional escrita en Java. Es muy rápida, de código abierto y se puede ejecutar en modo embebido o en modo servidor: https://hsqldb.org/
URL: jdbc:hsqldb:mem:testdb (base de datos en memoria)
Driver: org.hsqldb.jdbc.JDBCDriver
URL para servidor: jdbc:hsqldb:hsql://localhost/testdb
URL para fichero: jdbc:hsqldb:file:nombrebasededatos
5. Dependencias para JPA
5.1. Jakarta Persistence API (JPA)
La Java Persistence API (JPA) es una especificación de Java que describe la gestión de la persistencia de los objetos en las aplicaciones Java. JPA define un conjunto de interfaces y anotaciones que permiten a los desarrolladores mapear objetos Java a tablas de bases de datos y viceversa. Puedes encontrar más información en la página oficial de JPA:
Hibernate es un framework de mapeo objeto-relacional (ORM) para Java. Hibernate simplifica el desarrollo de aplicaciones Java que interactúan con bases de datos relacionales. Puedes encontrar más información en la página oficial de Hibernate: https://hibernate.org/
Pronto se lanzará la versión final de Hibernate 7, que será compatible con JPA 3.2. Esperamos.
5.3. EclipseLink
EclipseLink es otro framework de mapeo objeto-relacional (ORM) para Java. EclipseLink es una implementación de la especificación JPA y proporciona una serie de características avanzadas, como el mapeo de herencia, el mapeo de tablas, el mapeo de relaciones y la consulta de objetos. Puedes encontrar más información en la página oficial de EclipseLink: https://www.eclipse.org/eclipselink/
Pronto se lanzará la versión final de EclipseLink 5, que será compatible con JPA 3.2. Esperamos.
6. Dependencias para Spring
6.1. Spring Core
Spring Core es el núcleo del framework Spring. Proporciona las funcionalidades básicas de Spring, como la inyección de dependencias y la gestión de transacciones. Puedes encontrar más información en la página oficial de Spring: https://spring.io/projects/spring-framework
Spring Boot es un proyecto de Spring que simplifica el desarrollo de aplicaciones Java. Proporciona una serie de características, como la configuración automática, el embebido de servidores, la gestión de dependencias y la creación de aplicaciones ejecutables. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot
Spring Boot Starter es una colección de dependencias que se utilizan comúnmente en las aplicaciones Spring Boot. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot
Spring Data JPA es un proyecto de Spring que simplifica el acceso a datos en aplicaciones Java. Proporciona una serie de características, como la creación de repositorios, la generación de consultas y la gestión de transacciones. Puedes encontrar más información en la página oficial de Spring Data JPA: https://spring.io/projects/spring-data-jpa
Spring Boot Starter Data JPA es una colección de dependencias que se utilizan comúnmente en las aplicaciones Spring Boot que utilizan Spring Data JPA. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot
Kotlin es un lenguaje de programación moderno y conciso que se ejecuta sobre la JVM. Kotlin es interoperable con Java, lo que significa que puedes utilizar las bibliotecas de Java en Kotlin y viceversa. Puedes encontrar más información en la página oficial de Kotlin: https://kotlinlang.org/
IDEs como IntelliJ IDEA o Android Studio soportan Kotlin de forma nativa, pero también puedes usar Kotlin en otros IDE añadiendo las dependencias necesarias.
Scala es un lenguaje de programación funcional y orientado a objetos que se ejecuta sobre la JVM. Scala es interoperable con Java, lo que significa que puedes utilizar las bibliotecas de Java en Scala y viceversa. Puedes encontrar más información en la página oficial de Scala: https://www.scala-lang.org/
IDEs como IntelliJ IDEA o Eclipse soportan Scala de forma nativa, pero también puedes usar Scala en otros IDE añadiendo las dependencias necesarias.
La API de Streams de Java ofrece un enfoque funcional para el procesamiento de colecciones de objetos. Se introdujo en Java 8 junto con varias otras características de programación funcional. Este tutorial de Java Stream explicará cómo funcionan estos streams funcionales y cómo usarlos.
La API de Java Stream no está relacionada con Java InputStream y Java OutputStream de Java IO. InputStream y OutputStream se relacionan con flujos de bytes, mientras que la API de Stream de Java se utiliza para procesar flujos de objetos.
1. Definición de Java Stream
Un Stream en Java es un componente capaz de realizar una iteración interna de sus elementos, lo que significa que puede iterar sobre sus elementos por sí mismo. En contraste, al usar las características de iteración de Java Collections (por ejemplo, un Java Iterator o el bucle for-each de Java utilizado con un Java Iterable), debes implementar la iteración de los elementos tú mismo.
2. Procesamiento de Streams
Puedes adjuntar oyentes a un Stream. Estos oyentes se llaman cuando el Stream itera internamente los elementos. Los oyentes se llaman una vez por cada elemento en el stream. De esta manera, cada oyente procesa cada elemento en el stream. Esto se denomina procesamiento de stream.
Los oyentes de un stream forman una cadena. El primer oyente en la cadena puede procesar el elemento en el stream y luego devolver un nuevo elemento para que el siguiente oyente en la cadena lo procese. Un oyente puede devolver el mismo elemento o uno nuevo, dependiendo del propósito de ese oyente (procesador).
3. Crear un Stream
Hay muchas formas de obtener un Stream en Java. Una de las formas más comunes de obtener un Stream es desde una Java Collection. Un ejemplo de cómo obtener un Stream desde una Java List:
Este ejemplo crea primero una lista de Java, luego agrega tres Java Strings y, finalmente, llama al método stream() para obtener una instancia de Stream.
4. Operaciones Terminales y No Terminales
La interfaz Stream tiene una selección de operaciones terminales y no terminales. Una operación no terminal de stream es una operación que agrega un oyente al stream sin hacer nada más. Una operación terminal de stream es una operación que inicia la iteración interna de los elementos, llama a todos los oyentes y devuelve un resultado.
Un ejemplo de Java Stream que contiene tanto una operación no terminal como una terminal:
En este ejemplo, filter es una operación no terminal que filtra elementos basándose en el predicado proporcionado. Luego, forEach es una operación terminal que itera sobre los elementos restantes y aplica la función de impresión.
5. Operaciones No Terminales
Las operaciones no terminales de stream devuelven un nuevo Stream. Estas operaciones no realizan ninguna iteración interna. Se ejecutan “perezosamente”, lo que significa que no realizan ninguna acción hasta que se activa una operación terminal.
filter()
La operación filter es una operación no terminal que acepta un Java Predicate)como argumento y devuelve un nuevo Stream que contiene solo los elementos que cumplen con el predicado.
List<String> listaDeCadenas = Arrays.asList("Uno", "Dos", "Tres", "Cuatro", "Cinco");
// Filtrar elementos que comienzan con "C"Stream<String> streamFiltrado = listaDeCadenas.stream().filter(s -> s.startsWith("C"));
// Imprimir elementosstreamFiltrado.forEach(System.out::println);
En este ejemplo, filter se utiliza para seleccionar solo las cadenas que comienzan con “C”.
map()
La operación map es una operación no termina que transforma cada elemento del Stream utilizando la función proporcionada como argumento. Devuelve un nuevo Stream que contiene los elementos transformados:
<R> Stream<R>map(Function<?super T,?extends R> mapper)
R apply(T t) //"método" de la interfaceFunction
En este ejemplo, map se utiliza para convertir cada cadena a mayúsculas.
flatMap()
La operación flatMap es una operación no terminal que transforma cada elemento del Stream en cero o más elementos según la función proporcionada como argumento. Luego, combina los elementos resultantes en un solo Stream.
<R> Stream<R>flatMap(Function<?super T,?extends Stream<?extends R>> mapper)
R apply(T t)
List<List<Integer>> numeros = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
// Obtener un solo Stream de todos los númerosStream<Integer> numerosStream = numeros.stream().flatMap(List::stream);
// Imprimir elementosnumerosStream.forEach(System.out::println);
En este ejemplo, flatMap se utiliza para obtener un solo Stream de todos los números en las listas anidadas.
distinct()
La operación distinct es una operación no terminal que elimina los elementos duplicados del Stream, basándose en su implementación del método equals().
En este ejemplo, distinct se utiliza para eliminar las cadenas duplicadas.
limit()
La operación limit es una operación no terminal que reduce la longitud del Stream a la cantidad especificada.
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange", "grape");
// Limitar a los primeros dos elementosStream<String> streamLimitado = listaDeCadenas.stream().limit(2);
// Imprimir elementosstreamLimitado.forEach(System.out::println);
En este ejemplo, limit se utiliza para limitar el Stream a los primeros dos elementos.
peek()
La operación peek es una operación no terminal que permite realizar un side effect en cada elemento del Stream, como imprimir el elemento antes de que se pase a la siguiente operación.
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");
// Imprimir cada elemento antes de la transformaciónList<String> resultado = listaDeCadenas.stream()
.peek(System.out::println)
.map(String::toUpperCase)
.collect(Collectors.toList());
En este ejemplo, peek se utiliza para imprimir cada elemento antes de que se transforme a mayúsculas.
6. Operaciones Terminales
Las operaciones terminales de stream inician la iteración interna de los elementos y devuelven un resultado final. Después de que se realiza una operación terminal, un Stream no puede ser utilizado de nuevo.
anyMatch()
La operación anyMatch es una operación terminal que devuelve true si al menos un elemento del Stream cumple con la condición dada, de lo contrario, devuelve false.
En este ejemplo, anyMatch se utiliza para verificar si alguna cadena contiene la subcadena “nan”.
allMatch()
La operación allMatch es una operación terminal que devuelve true si todos los elementos del Stream cumplen con la condición dada, de lo contrario, devuelve false.
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");
// Verificar si todas las cadenas tienen longitud mayor que 3boolean todasLargas = listaDeCadenas.stream().allMatch(s -> s.length() > 3);
System.out.println(todasLargas); // Salida: true
En este ejemplo, allMatch se utiliza para verificar si todas las cadenas tienen una longitud mayor que 3.
noneMatch()
La operación noneMatch es una operación terminal que devuelve true si ninguno de los elementos del Stream cumple con la condición dada, de lo contrario, devuelve false.
En este ejemplo, noneMatch se utiliza para verificar si ninguna cadena contiene la subcadena “grape”.
findFirst()
La operación findFirst es una operación terminal que devuelve el primer elemento del Stream en un Optional. Si el Stream está vacío, devuelve un Optional vacío.
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");
// Obtener el primer elementoOptional<String> primerElemento = listaDeCadenas.stream().findFirst();
primerElemento.ifPresent(System.out::println); // Imprimir el primer elemento si está presente
En este ejemplo, findFirst se utiliza para obtener el primer elemento de la lista.
findAny()
La operación findAny es una operación terminal que devuelve cualquier elemento del Stream en un Optional. Si el Stream está vacío, devuelve un Optional vacío.
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");
// Obtener cualquier elementoOptional<String> cualquierElemento = listaDeCadenas.stream().findAny();
cualquierElemento.ifPresent(System.out::println); // Imprimir cualquier elemento si está presente
En este ejemplo, findAny se utiliza para obtener cualquier elemento de la lista.
forEach()
La operación forEach es una operación terminal que ejecuta una acción para cada elemento del Stream.
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");
// Imprimir cada elementolistaDeCadenas.stream().forEach(System.out::println);
En este ejemplo, forEach se utiliza para imprimir cada elemento de la lista.
collect()
La operación collect es una operación terminal que transforma los elementos del Stream en una estructura de datos diferente, como una List, Set, o Map. Se utiliza junto con la interfaz Collector.
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");
// Recoger elementos en una ListList<String> listaRecolectada = listaDeCadenas.stream().collect(Collectors.toList());
// Imprimir elementoslistaRecolectada.forEach(System.out::println);
En este ejemplo, collect se utiliza para recolectar los elementos en una List.
count()
La operación count es una operación terminal que devuelve el número de elementos en el Stream.
En este ejemplo, count se utiliza para contar el número de elementos en la lista.
reduce()
La operación reduce es una operación terminal que combina los elementos del Stream en un solo resultado mediante una función asociativa y un valor identidad. Puede devolver un Optional si el Stream está vacío.
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);
// Sumar todos los elementosOptional<Integer> suma = numeros.stream().reduce(Integer::sum);
suma.ifPresent(System.out::println); // Imprimir la suma si está presente
En este ejemplo, reduce se utiliza para sumar todos los elementos de la lista.
min() y max()
Las operaciones min y max son operaciones terminales que devuelven el elemento mínimo y máximo del Stream, respectivamente, basándose en el orden natural o un comparador proporcionado.
List<Integer> numeros = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);
// Encontrar el mínimo y el máximoOptional<Integer> minimo = numeros.stream().min(Comparator.naturalOrder());
Optional<Integer> maximo = numeros.stream().max(Comparator.naturalOrder());
minimo.ifPresent(System.out::println); // Imprimir el mínimo si está presentemaximo.ifPresent(System.out::println); // Imprimir el máximo si está presente
En este ejemplo, min y max se utilizan para encontrar el elemento mínimo y máximo de la lista.
forEachOrdered()
La operación forEachOrdered es similar a forEach, pero garantiza que los elementos se procesen en orden en un Stream paralelo.
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");
// Imprimir cada elemento en orden en un Stream paralelolistaDeCadenas.parallelStream().forEachOrdered(System.out::println);
En este ejemplo, forEachOrdered se utiliza para imprimir cada elemento en orden en un Stream paralelo.
Resumen
En Java, los Streams proporcionan una forma declarativa y funcional de procesar colecciones de datos. Las operaciones de stream se dividen en dos categorías: operaciones no terminales y operaciones terminales. Las operaciones no terminales se componen para formar una cadena de operaciones que se ejecutan perezosamente. La ejecución se inicia solo cuando se encuentra una operación terminal.
Las operaciones no terminales, como filter, map, flatMap, distinct, limit, y peek, permiten filtrar, transformar y manipular los elementos del Stream. Las operaciones terminales, como anyMatch, allMatch, noneMatch, findFirst, findAny, forEach, collect, count, reduce, min, max, y forEachOrdered, producen un resultado final o realizan una acción final en los elementos del Stream.
Al aprovechar las operaciones de stream, puedes escribir código más conciso, legible y eficiente cuando trabajas con colecciones de datos en Java.
Muchos de los siguientes ejercicios trabajan con archivos JSON de APIs JSON públicas o abiertas de Internet.
Existen varios modos de acceder a recursos, archivos, de Internet:
Java IO.
Java NIO.
Bibliotecas externas como AsyncHttpClient y Apache Commons IO.
2. Java IO
La API más básica que podemos usar para descargar un archivo es Java IO. Podemos utilizar la clase URL para abrir una conexión al archivo que queremos descargar.
Para leer de manera eficaz el archivo, podemos utilizar el método openStream() para obtener un InputStream:
BufferedInputStream in =new BufferedInputStream(
new URL(FILE_URL).openStream())
Al leer desde un InputStream, se recomienda encapsularlo en un BufferedInputStream para aumentar el rendimiento.
Como hemos visto, la mejora de rendimiento proviene del almacenamiento en búfer. Al leer un byte a la vez mediante el método read(), cada llamada al método implica una llamada al sistema al sistema de archivos subyacente. Cuando la JVM invoca la llamada al sistema read(), el contexto de ejecución del programa cambia de modo usuario a modo kernel y viceversa. Este cambio de contexto es costoso desde una perspectiva de rendimiento. Al leer un gran número de bytes, el rendimiento de la aplicación será deficiente debido al gran número de cambios de contexto involucrados.
Para escribir los bytes leídos desde la URL en nuestro archivo local, utilizaremos el método write() de la clase FileOutputStream:
try (BufferedInputStream in =new BufferedInputStream(
new URL(FILE_URL).openStream());
FileOutputStream fileOutputStream =new FileOutputStream(FILE_NAME)) {
byte dataBuffer[]=newbyte[1024];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, 1024)) !=-1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
}
} catch (IOException e) {
// gestión de la excepción}
Cuando se utiliza un BufferedInputStream, el método read() leerá tantos bytes como hayamos establecido para el tamaño del búfer. En nuestro ejemplo, ya estamos haciendo esto al leer bloques de 1024 bytes a la vez, por lo que BufferedInputStream no es necesario. Para archivos JSON, como son archivos de texto, precisamos convertir el flujo entrada (InputStream) en un Reader por medio de la clase InputStreamReader, pasando a leer línea a línea.
Muchos métodos de procesado de archivos JSON (fromJson, parse,…) tienen una versión sobrecargada que recoge un Reader además de un String.
3. Java NIO
En el caso anterior, bajamos a nivel de flujo pero, como hemos estudiado, a partir de Java 7, se dispone de la clase Files que contiene métodos auxiliares para manejar operaciones de entrada/salida (IO).
Se puede utilizar el método Files.copy() para leer todos los bytes de un InputStream y copiarlos a un archivo local:
InputStream in =new URL(FILE_URL).openStream();
Files.copy(in, Paths.get(FILE_NAME),
StandardCopyOption.REPLACE_EXISTING);
El ejemplo anterior funciona bien, pero puede mejorarse. El principal ¿inconveniente? es que los bytes se almacenan en búfer en la memoria.
Java NIO tiene métodos para transferir bytes directamente entre dos canales sin almacenamiento en búfer.
El paquete Java NIO ofrece la posibilidad de transferir bytes entre dos canales sin almacenarlos en el espacio de memoria de la aplicación.
Para leer el archivo desde nuestra URL, crearemos un ReadableByteChannel a partir del flujo de URL:
Los métodos transferTo() y transferFrom() son más eficientes que simplemente leer desde un flujo utilizando un búfer. Dependiendo del sistema operativo subyacente, los datos pueden transferirse directamente desde la caché del sistema de archivos a nuestro archivo sin copiar ningún byte en el espacio de memoria de la aplicación.
En sistemas Linux y UNIX, estos métodos utilizan la técnica de copia cero que reduce el número de cambios de contexto entre el modo kernel y el modo usuario.
Debes seimpre sobreescribir hashCode en cada clase que sobrescriba equals. Si no lo haces se violará el contrato general de hashCode, lo que hará que NO funcione correctamente en colecciones como HashMap y HashSet. Aquí está el contrato, adaptado de la especificación de Object:
Cuando se invoca el método hashCode en un objeto repetidamente durante la ejecución de una aplicación, debe devolver consistentemente el mismo valor, siempre que la información utilizada en las comparaciones de equals no se modifique. Este valor no necesita ser consistente de una ejecución de una aplicación a otra.
Si dos objetos son iguales según el método equals(Object), llamar a hashCode en los dos objetos debe producir el mismo resultado entero.
Si dos objetos son diferentes según el método equals(Object), no es necesario que llamar a hashCode en cada uno de los objetos produzca resultados distintos. Sin embargo, el programador debe ser consciente de que producir resultados distintos para objetos diferentes puede mejorar el rendimiento de las tablas hash.
La disposición clave que se viola cuando no se sobrescribe hashCode es la segunda: los objetos iguales deben tener códigos hash iguales.
Dos instancias distintas pueden ser lógicamente iguales según el método equals de una clase, pero el método hashCode de Object, son sólo dos objetos con poco en común. Por lo tanto, el método hashCode de Object devuelve dos números aparentemente aleatorios en lugar de dos números iguales, como exige el contrato.
Por ejemplo, añadimos una instancia de la clase NumeroTelefono como clave en un HashMap:
Map<NumeroTelefono, String> m =new HashMap<>();
m.put(new NumeroTelefono(707, 867, 5309), "Otto");
En este punto, podrías esperar que m.get(new NumeroTelefono(707, 867, 5309)) devuelva “Otto”, pero en cambio, devuelve null. Observa que hay dos instancias de NumeroTelefono involucradas: una se utiliza para la inserción en el HashMap, y una segunda instancia igual se utiliza para la recuperación (intentada). La falta de sobrescritura de hashCode en la clase NumeroTelefono hace que las dos instancias iguales tengan códigos hash diferentes, violando el contrato de hashCode. Por lo tanto, es probable que el método get busque el número de teléfono en una tabla hash diferente al que fue almacenado por el método put. Incluso si las dos instancias resultan en el mismo cubo hash, es muy probable que el método get devuelva null, porque HashMap tiene una optimización que almacena en caché el código hash asociado con cada entrada y no se molesta en verificar la igualdad de objetos si los códigos hash no coinciden.
Para solucionar este problema, es tan sencillo como escribir un método hashCode adecuado para NumeroTelefono. Entonces, ¿cómo debería implementarse un método hashCode? Es fácil escribir uno malo. Este, por ejemplo, siempre es legal pero nunca debería ser usado:
// La peor implementación legal posible de hashCode: ¡nunca uses esto!@OverridepublicinthashCode() {
return 42;
}
Es legal porque asegura que los objetos iguales tengan el mismo código hash. Es muy nmalo porque asegura que cada objeto tenga el mismo código hash. Por lo tanto, cada objeto se asigna a la misma tabla hash, y las tablas hash degeneran en listas enlazadas. Los programas que deberían ejecutarse en tiempo lineal en su lugar se ejecutan en tiempo cuadrático. Para tablas hash grandes, esta es la diferencia entre funcionar y no funcionar.
Una buena función de hash tiende a producir códigos hash diferentes para instancias diferentes. Esto es exactamente lo que se entiende por la tercera parte del contrato de hashCode. Idealmente, una función de hash debería distribuir uniformemente cualquier colección razonable de instancias diferentes en todos los valores int. Lograr este ideal puede ser difícil, pero afortunadamente, no es demasiado difícil lograr una aproximación justa. Aquí tienes una receta sencilla:
Declara una variable int llamada resultado y inicialízala con el código hash c para el primer campo significativo en tu objeto, calculado en el paso 2.1. (Un campo significativo es un campo que afecta las comparaciones de equals).
Para cada campo significativo restante f en tu objeto, realiza lo siguiente:
2.1 Calcula un código hash c para el campo:
i. Si el campo es de un tipo primitivo, calcula Type.hashCode(f), donde Type es la clase primitiva encapsulada correspondiente al tipo de f.
Si el campo es una referencia a un objeto y el método equals de esta clase compara el campo mediante la invocación recursiva de equals, invoca recursivamente hashCode en el campo. Si se requiere una comparación más compleja, calcula una “representación canónica” para este campo e invoca hashCode en la representación canónica. Si el valor del campo es null, utiliza 0 (u otra constante, pero 0 es tradicional).
Si el campo es un array, trátalo como si cada elemento significativo fuera un campo separado. Es decir, calcula un código hash para cada elemento significativo aplicando estas reglas recursivamente y combina los valores según el paso 2.b. Si el array no tiene elementos significativos, utiliza una constante, preferiblemente no 0. Si todos los elementos son significativos, utiliza Arrays.hashCode.
Combina el código hash c calculado en el paso 2.a en resultado de la siguiente manera:
resultado = 31 * resultado + c;
Devuelve resultado.
Cuando hayas terminado de escribir el método hashCode, pregúntate a ti mismo si las instancias iguales tienen códigos hash iguales. Escribe pruebas unitarias para verificar tu intuición (a menos que hayas utilizado AutoValue para generar tus métodos equals y hashCode, en cuyo caso puedes omitir estas pruebas de manera segura). Si las instancias iguales tienen códigos hash diferentes, averigua por qué y soluciona el problema.
Puedes excluir campos derivados del cálculo del código hash. En otras palabras, puedes ignorar cualquier campo cuyo valor pueda calcularse a partir de campos incluidos en el cálculo. Debes excluir cualquier campo que no se utilice en comparaciones de equals, o corres el riesgo de violar la segunda disposición del contrato de hashCode.
La multiplicación en el paso 2.b hace que el resultado dependa del orden de los campos, lo que produce una función de hash mucho mejor si la clase tiene múltiples campos similares. Por ejemplo, si se omitiera la multiplicación de una función de hash de String, todos los anagramas tendrían códigos hash idénticos. Se eligió el valor 31 porque es un número primo impar. Si fuera par y la multiplicación desbordara, se perdería información, ya que la multiplicación por 2 es equivalente a un desplazamiento. La ventaja de usar un número primo es menos clara, pero es tradicional. Una propiedad agradable del 31 es que la multiplicación se puede reemplazar por un desplazamiento y una resta para obtener un mejor rendimiento en algunas arquitecturas: 31 * i == (i << 5) - i. Las VM modernas realizan este tipo de optimización automáticamente.
Aplicaremos la receta anterior a la clase NumeroTelefono:
Dado que este método devuelve el resultado de un cálculo determinista simple cuyas únicas entradas son los tres campos significativos en una instancia de NumeroTelefono, es evidente que las instancias iguales de NumeroTelefono tienen códigos hash iguales. Este método es, de hecho, una implementación de hashCode perfectamente buena para NumeroTelefono, al nivel de las bibliotecas de plataformas Java. Es simple, bastante rápido y hace un trabajo razonable al dispersar números de teléfono diferentes en diferentes cubos hash.
Si bien esta estrategia produce funciones de hash bastante buenas, no son de última generación. Son comparables en calidad a las funciones de hash que se encuentran en los tipos de valor de las bibliotecas de plataformas Java y son adecuadas para la mayoría de los usos. Si realmente necesitas funciones de hash menos propensas a producir colisiones, consulta la clase com.google.common.hash.Hashing de Guava [Guava].
La clase Objects tiene un método estático que recoge un número arbitrario de objetos y devuelve un código hash para ellos. Este método, llamado hash, te permite escribir métodos hashCode de una línea cuya calidad es comparable a los declarados anteriormente. Desafortunadamente, se ejecutan más lentamente porque implican la creación de un array para pasar un número variable de argumentos, así como el boxing y unboxing si alguno de los argumentos es de tipo primitivo. Este estilo de función de hash se recomienda sólo en situaciones donde el rendimiento no es crítico. Aquí tienes una función de hash para NumeroTelefono escrita utilizando esta técnica:
// Método hashCode de una línea - rendimiento mediocre@OverridepublicinthashCode() {
return Objects.hash(numeroT, prefijo, codigoArea);
}
Si una clase es inmutable y el costo de calcular el código hash es significativo, podrías considerar almacenar en caché el código hash en el objeto en lugar de recalcularlo cada vez que se solicita. Si crees que la mayoría de los objetos de este tipo se utilizarán como claves hash, deberías calcular el código hash cuando se crea la instancia. De lo contrario, podrías optar por inicializar perezosamente el código hash la primera vez que se invoca hashCode. Se requiere cierto cuidado para asegurar que la clase siga siendo segura para subprocesos en presencia de un campo inicializado de forma perezosa. Nuestra clase NumeroTelefono no merece este tratamiento, pero solo para mostrarte cómo se hace, aquí está. Ten en cuenta que el valor inicial para el campo hashCode (en este caso, 0) no debería ser el código hash de una instancia comúnmente creada:
// Método hashCode con caché de código hash inicializado perezosamenteprivateint hashCode; // Inicializado automáticamente a 0@OverridepublicinthashCode() {
int resultado = hashCode;
if (resultado == 0) {
resultado = Short.hashCode(codigoArea);
resultado = 31 * resultado + Short.hashCode(prefijo);
resultado = 31 * resultado + Short.hashCode(numeroT);
hashCode = resultado;
}
return resultado;
}
No te dejes tentar a excluir campos significativos del cálculo del código hash para mejorar el rendimiento. Aunque la función de hash resultante puede ejecutarse más rápido, su baja calidad puede degradar el rendimiento de las tablas hash hasta el punto en que se vuelven inutilizables. En particular, la función de hash puede enfrentarse a una gran colección de instancias que difieren principalmente en las regiones que has elegido ignorar. Si esto sucede, la función de hash asignará todas estas instancias a unos pocos códigos hash, y los programas que deberían ejecutarse en tiempo lineal en su lugar se ejecutarán en tiempo cuadrático.
Esto no es solo un problema teórico. Antes de Java 2, la función de hash de String utilizaba como máximo dieciséis caracteres distribuidos uniformemente en toda la cadena, comenzando por el primer carácter. Para grandes colecciones de nombres jerárquicos, como las URL, esta función mostraba exactamente el comportamiento patológico descrito anteriormente.
No proporciones una especificación detallada para el valor devuelto por hashCode, de modo que los clientes no puedan depender razonablemente de él; esto te da la flexibilidad para cambiarlo. Muchas clases en las bibliotecas de Java, como String e Integer, especifican el valor exacto devuelto por su método hashCode como una función del valor de la instancia. Esto no es una buena idea, sino un error con el que nos vemos obligados a vivir: obstaculiza la capacidad de mejorar la función de hash en futuras versiones. Si dejas los detalles sin especificar y se encuentra un defecto en la función de hash o se descubre una función de hash mejor, puedes cambiarla en una versión posterior.
En resumen, debes sobrescribir hashCode cada vez que sobres equals, o tu programa no funcionará correctamente. Tu método hashCode debe obedecer el contrato general especificado en Object y debe hacer un trabajo razonable asignando códigos hash diferentes a instancias diferentes. Esto es fácil de lograr, aunque ligeramente tedioso, utilizando la receta en la página 51. Como se menciona en el Ítem 10, el framework AutoValue proporciona una excelente alternativa para escribir manualmente los métodos equals y hashCode, y los IDE también ofrecen parte de esta funcionalidad.
Nota: nosotros aprenderemos a situar los elementos con código de diseño a mano, lo cual puede ser desafiante, pero muy útil para aprender conceptos y poder después tocar el código que proporcionan los IDE. Los IDE, como el Netbeans, suelen emplear un GroupLayout. Para codificar a mano el GridBagLayout se recomienda como el Layout más flexible y poderoso.
BorderLayout
Un BorderLayout coloca componentes en hasta cinco áreas: arriba, abajo, izquierda, derecha y centro. Todo el espacio adicional se coloca en el área central. Las barras de herramientas creadas con JToolBar deben ser creadas dentro de un contenedor BorderLayout si deseas poder arrastrar y soltar las barras desde sus posiciones iniciales.
BoxLayout
La clase BoxLayout coloca componentes en una sola fila o columna. Respeta los tamaños máximos solicitados por los componentes y también te permite alinear componentes.
CardLayout
La clase CardLayout permite implementar un área que contiene diferentes componentes en diferentes momentos. Un CardLayout suele ser controlado por un cuadro combinado, con el estado del cuadro combinado determinando qué panel (grupo de componentes) muestra el CardLayout. Una alternativa a usar CardLayout es usar un panel con pestañas, que proporciona una funcionalidad similar pero con una GUI predefinida. Por ello, lo más sencillo es utilizar un JTabbedPane y no este Layout.
FlowLayout
FlowLayout es el gestor de diseño predeterminado para cada JPanel. Simplemente distribuye los componentes en una sola fila, comenzando una nueva fila si su contenedor no es lo suficientemente ancho.
GridBagLayout
GridBagLayout es un gestor de diseño sofisticado y flexible. Alinea los componentes colocándolos dentro de una cuadrícula de celdas, permitiendo que los componentes abarquen más de una celda. Las filas en la cuadrícula pueden tener diferentes alturas, y las columnas de la cuadrícula pueden tener diferentes anchos.
GridLayout
GridLayout simplemente hace que un grupo de componentes en forma de rejilla tengan el mismo tamaño y los muestra en el número solicitado de filas y columnas.
GroupLayout (*)
GroupLayout es un gestor de diseño que fue desarrollado para ser usado por herramientas de construcción de GUI, pero también puede ser usado manualmente. GroupLayout trabaja con los diseños horizontal y vertical por separado. El diseño se define para cada dimensión independientemente. En consecuencia, cada componente necesita ser definido dos veces en el diseño.
SpringLayout (*)
SpringLayout es un gestor de diseño flexible diseñado para ser usado por constructores de GUI. Te permite especificar relaciones precisas entre los bordes de los componentes bajo su control. Por ejemplo, podrías definir que el borde izquierdo de un componente está a cierta distancia (que puede ser calculada dinámicamente) del borde derecho de un segundo componente. SpringLayout distribuye los hijos de su contenedor asociado según un conjunto de restricciones.
1. BorderLayout
Cada contenedor de nivel superior (JFrame, JDialog,… ) de contenido se inicializa por defecto a BorderLayout.
Un BorderLayout coloca componentes en hasta cinco áreas: superior, inferior, izquierda, derecha y centro. Todo el espacio adicional se coloca en el área central. Las barras de herramientas creadas usando JToolBar deben crearse dentro de un contenedor BorderLayout si se desea poder arrastrar y soltar las barras desde sus posiciones iniciales.
2. BoxLayout
La clase BoxLayout coloca los componentes en una sola fila o columna. Respeta los tamaños máximos solicitados por los componentes y también permite alinear los componentes.
3. CardLayout
La clase CardLayout te permite implementar un área que contiene diferentes componentes en diferentes momentos. Un CardLayout suele ser controlado por un cuadro combinado (combo box), con el estado del cuadro combinado determinando qué panel (grupo de componentes) muestra el CardLayout. Una alternativa a usar CardLayout es usar un panel con pestañas, un JTabbedPane, que proporciona una funcionalidad similar pero con una GUI predefinida.
4. FlowLayout
FlowLayout es el gestor de diseño predeterminado para cada JPanel. Simplemente organiza los componentes en una sola fila, comenzando una nueva fila si su contenedor no es lo suficientemente ancho. Ambos paneles en CardLayoutDemo, mostrados anteriormente, usan FlowLayout.
5. GridBagLayout
GridBagLayout es un gestor de diseño sofisticado y flexible. Alinea los componentes colocándolos dentro de una cuadrícula de celdas, permitiendo que los componentes abarquen más de una celda. Las filas en la cuadrícula pueden tener diferentes alturas y las columnas de la cuadrícula pueden tener diferentes anchos.
6. GridLayout
GridLayout simplemente hace que un montón de componentes sean del mismo tamaño y los muestra en el número solicitado de filas y columnas.
7. GroupLayout
GroupLayout es un gestor de diseño que fue desarrollado para su uso por herramientas de construcción de GUI, pero también puede ser utilizado manualmente. GroupLayout trabaja con los diseños horizontales y verticales por separado. El diseño se define para cada dimensión de manera independiente. Por lo tanto, cada componente necesita ser definido dos veces en el diseño. La ventana de búsqueda mostrada anteriormente es un ejemplo de un GroupLayout.
8. SpringLayout
SpringLayout es un gestor de diseño flexible diseñado para ser utilizado por constructores de GUI. Permite especificar relaciones precisas entre los bordes de los componentes bajo su control. Por ejemplo, se puede definir que el borde izquierdo de un componente esté a cierta distancia (que puede calcularse dinámicamente) del borde derecho de un segundo componente. SpringLayout organiza los hijos de su contenedor asociado de acuerdo a un conjunto de restricciones.
Subsecciones de 03. Layouts en Java Swing & UI
01 Ventanas de entrada de datos, mensajes y archivos
JFC (Java Foundation Classes) es un conjunto de funciones/clases para crear interfaces gráficas de usuario (GUI) y añadir funcionalidad gráfica e interactividad a las aplicaciones Java que estudiaréis en la materia de Desenvolvemento de Interfaces.
Incluye: componentes gráficos de Swing (botones, paneles, tablas, ventanas, etc.), configuración de apariencia, API Java 2D, API de accesibilidad (lectores de pantalla, Braille,…); internacionalización (gestión de idiomas del mundo, …)
En la materia de Acceso a Datos vamos a dar una pequeña introducción a dos componentes de diálogo que nos facilitarán la introducción de datos en la primera parte, mostrar mensajes o selección de ficheros hasta que lo estudiéis en otras materias:
JOptionPane permite crear y personalizar rápidamente varios tipos diferentes de cuadros de diálogo.
Proporciona soporte para diseñar cuadros de diálogo estándar, con iconos, especificar el título y el texto del cuadro de diálogo y personalizar el texto del botón.
La compatibilidad con iconos de le permite especificar fácilmente qué icono muestra el cuadro de diálogo.
Son modales, esto es, bloquea el acceso a la ventana padre.
Puede utilizar un icono personalizado, ningún icono o cualquiera de los cuatro iconos estándar:
Cada apariencia tiene sus propias versiones de los cuatro íconos estándar.
La forma más sencilla de crear y mostrar diálogos con JOptionPane es por medio de los métodos showXxxDialog:
Método
Descripción
showConfirmDialog
Pregunta de confirmación, como sí/no/cancelar.
showInputDialog
Solicita entrada de datos.
showMessageDialog
Muestra un mensaje informativo
showOptionDialog
Permite personalizar el JOptionPane.
Existe una versión para marcos internos, JInternalFrame con métodos de la forma: showInternalXxxx
El formato del mensaje tiene una apariencia siguiendo la siguiente estructura:**
Los parámetros de los método showXxxDialog son los siguientes:
parentComponent: componente padre, el JFrame. Si el valor es null, se usa el JFrame por defecto y se centra en la pantalla.
mensaje: mensaje de la ventana de diálogo. Normalmente String, pero puede ser cualquier objeto:
Object[]: será interpretado como una serie de mensajes (uno
por objeto) situados en vertical.
Component: mostrará el componente en la ventana de diálogo.
Icon: el icono será mostrado dentro de un JLabel.
Otros: se convierten a String llamando al método toString() y mostrándolo dentro de un JLabel.
Título: título de la ventana.
messageType: define el estilo del mensaje. Los posibles valores son:
ERROR_MESSAGE
INFORMATION_MESSAGE
WARNING_MESSAGE
QUESTION_MESSAGE
PLAIN_MESSAGE
optionType: conjunto botones de opción que aparen debajo de la ventana.
Se pueden proporcionar otro botones usando el parámetro options.
DEFAULT_OPTION
YES_NO_OPTION
YES_NO_CANCEL_OPTION
OK_CANCEL_OPTION
options: es una descripción más detallada del conjunto de botones que aparecen en la parte inferior de la ventana. Lo usual es un array de String, pero puede ser un array de Object:
Component: el componente se añade a la lista de botones.
Icon: se crea un JButton con este icono.
Otros: el objeto se convierte en String (toString()) y se emplea como etiqueta del botón.
Icono: icono de la ventana de diálogo. El valor por defecto está determinado por el tipo de mensaje.
initialValue: valor por defecto para ventanas de tipo input.
Los métodos showXxxDialog devuelven un entero, cuyos valores posibles son las constantes que referencian al botón pulsado:
YES_OPTION
NO_OPTION
CANCEL_OPTION
OK_OPTION
CLOSED_OPTION
Object[] opcionesBoton = {"Sí, por supuesto", "No, gracias", "No estoy loco!"};
int resultado = JOptionPane.showOptionDialog(this, "¿Te has vacunado de COVID?", // mensaje"Una pregunta impertinente", // título JOptionPane.YES_NO_CANCEL_OPTION, // OptionType JOptionPane.QUESTION_MESSAGE, // Tipo de mensaje null, // icono opcionesBoton,
opcionesBoton[2]); // valor por defecto
showMessageDialog
showMessageDialog : muestra un mensaje simple con un botón. (this es la referencia al formulario)
JOptionPane.showMessageDialog(this, "Hola Pepe",
"Un saludo a Harry Haller",
JOptionPane.INFORMATION_MESSAGE);
Con título e icono por defecto:
JOptionPane.showMessageDialog(this, "Ovos fritidos con paracas.");
Con título, icono de aviso:
JOptionPane.showMessageDialog(this, "Arroz con chícharos.", "Aviso", JOptionPane.WARNING_MESSAGE);
Con título, icono de error:
JOptionPane.showMessageDialog(this, "Repolo de Vimianzo.", "Erro", JOptionPane.ERROR_MESSAGE);
showInputDialog : es el único método showXxxDialog que no devuelve un entero.
Devuelve un objeto, normalmente un String:
Object[] platoFavorito = {"chícharos", "doce", "churros"};
String s = (String)JOptionPane.showInputDialog(this, "o meu prato favorito é: ",
"Introduce tu plato favorito", JOptionPane.PLAIN_MESSAGE, iconaSalada, platoFavorito, "churros");
Si ponemos null en el array de opciones aparecerá una caja de texto:
Devuelve un objeto, normalmente un String:
String s = (String)JOptionPane.showInputDialog(this,
"o meu prato favorito é: ", "Introduce tu plato favorito", JOptionPane.PLAIN_MESSAGE, iconaSalada,
null, "churros");
4. JFileChooser
Los selectores de archivos proporcionan una GUI para navegar por el sistema de archivos y luego elegir un archivo o directorio de una lista, o introducir el nombre de un archivo o directorio.
Normalmente usa la clase JFileChooser para mostrar un cuadro de diálogo modal que contiene el selector de archivos. Otra forma de presentar un selector de archivos es agregar una instancia de JFileChooser a un contenedor (ventana etc)
Ejemplo:
JFileChooser fc =new JFileChooser();
int returnVal = fc.showOpenDialog(this);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = fc.getSelectedFile();
// Esto es lo que se hace con el archivo seleccionado} else {
// Esto es lo que se hace si se cancela la selección}
El método showOpenDialog muestra un cuadro de diálogo para abrir un archivo.
El método showSaveDialog muestra un cuadro de diálogo para guardar un archivo.
JFileChooser fcImaxe =new JFileChooser();
FileNameExtensionFilter filtro
=new FileNameExtensionFilter("Imágenes JPG y PNG", "jpg", "png");
fcImaxe.setFileFilter(filtro);
int valorSel = fcImaxe.showOpenDialog(null);
if (valorSel == JFileChooser.APPROVE_OPTION){
System.out.println("Has seleccionado la imagen:"+ fcImaxe.getSelectedFile().getName());
}
Directorio de trabajo
showOpenDialog recoge el componente padre de la ventana de diálogo y afecta a la posición de la ventana de diálogo.
Por defecto muestra los archivos del directorio de trabajo del usuario, pero puede especificarse el directorio inicial de varios modos:
En el constructor:
JFileChooser fc =new JFileChooser("e:\\");
Por medio del método:
fc.setCurrentDirectory(new File("e:\\"));
Selección de archivos y/o directorios
showOpenDialog/showSaveDialog recoge devuelven un entero que indica si se ha seleccionado un archivo: APPROVE_OPTION o CANCEL_OPTION
Una vez seleccionado un archivo o directorio (en ese caso debe indicarse que se permite selección de directorios) puede invocarse al método getSelectedFile() para recuperar el archivo (File):
File arquivo = fcImaxe.getSelectedFile();
Una vez recuperado el archivo podemos obtener muchos datos del mismo (lo veremos en la unidad de archivos):
FileFilter filtro =new FileNameExtensionFilter("archivo JPEG", "jpg", "jpeg");
JFileChooser fc; // = ...; fc.setFileFilter(filtro);
// fc.addChoosableFileFilter(filtro); // agrega a los seleccionables.// fc. setAcceptAllFileFilterUsed(false);
05 GridBagLayout
GridBagLayout
Cómo usar GridBagLayout
GridBagLayout es uno de los gestores de diseño más flexibles —y complejos— que proporciona la plataforma Java. Un GridBagLayout coloca componentes en una cuadrícula de filas y columnas, permitiendo que los componentes especificados abarquen múltiples filas o columnas.
No todas las filas necesariamente tienen la misma altura. Del mismo modo, no todas las columnas necesariamente tienen el mismo ancho. Esencialmente, GridBagLayout coloca los componentes en rectángulos (celdas) en una cuadrícula y luego utiliza los tamaños preferidos de los componentes para determinar qué tan grandes deben ser las celdas.
La forma en que el programa especifica las características de tamaño y posición de sus componentes es especificando restricciones para cada componente. El enfoque preferido para establecer restricciones en un componente es usar la variante de Container.add, pasándole un objeto GridBagConstraints.
GridBagLayout y GridBagConstraints
GridBagConstraints es una clase que especifica cómo se colocan los componentes en un GridBagLayout. Cada componente que se agrega a un contenedor con un GridBagLayout debe tener su propio objeto GridBagConstraints.
GridBagConstraints tiene muchas propiedades que se pueden establecer para controlar cómo se coloca un componente en un GridBagLayout. Algunas de las propiedades más comunes son:
gridx, gridy: la posición de la celda en la cuadrícula donde se colocará el componente.
gridwidth, gridheight: el número de celdas en la cuadrícula que el componente debe ocupar en la dirección horizontal y vertical.
weightx, weighty: la cantidad de espacio adicional que se asigna a la celda en la dirección horizontal y vertical.
fill: cómo el componente debe llenar la celda si la celda es más grande que el componente.
anchor: cómo el componente debe ser posicionado en la celda si la celda es más grande que el componente.
insets: el espacio adicional que se debe agregar alrededor del componente.
ipadx, ipady: el espacio adicional que se debe agregar alrededor del componente en la dirección horizontal y vertical.
Por ejemplo, el siguiente código Swing crea un GridBagLayout con un botón en la esquina superior izquierda de la cuadrícula:
En este ejemplo, se crea un GridBagLayout y se agrega un botón al panel. El botón se coloca en la esquina superior izquierda de la cuadrícula, ya que gridx y gridy se establecen en 0.
También podríamos crear un rejilla de 3x3 y añadir un botón en la posición (1,1) de la cuadrícula:
En este ejemplo, se crean tres botones y se agregan al panel. El primer botón se coloca en la posición (0,0) de la cuadrícula, el segundo botón se coloca en la posición (1,0) de la cuadrícula y el tercer botón se coloca en la posición (0,1) de la cuadrícula y ocupa dos celdas en la dirección horizontal.
Por último, podemos crear un rejilla de dos filas y hacer que la segunda columna ocupe dos celdas en la dirección vertical:
En este ejemplo, se crean tres botones y se agregan al panel. El primer botón se coloca en la posición (0,0) de la cuadrícula, el segundo botón se coloca en la posición (1,0) de la cuadrícula y el tercer botón se coloca en la posición (0,1) de la cuadrícula y ocupa dos celdas en la dirección vertical.
anchor y fill
Además de las propiedades gridx, gridy, gridwidth, gridheight, weightx, weighty, insets, ipadx e ipady, GridBagConstraints también tiene las propiedades anchor y fill.
La propiedad anchor controla cómo se posiciona un componente en su celda si la celda es más grande que el componente. Los valores posibles para anchor son:
GridBagConstraints.FIRST_LINE_START (GridBagConstraints.NORTHWEST): el componente se coloca en la esquina superior izquierda de la celda.
GridBagConstraints.PAGE_START (GridBagConstraints.NORTH): el componente se coloca en la parte superior de la celda.
GridBagConstraints.FIRST_LINE_END (GridBagConstraints.NORTHEAST): el componente se coloca en la esquina superior derecha de la celda.
GridBagConstraints.LINE_START (GridBagConstraints.WEST): el componente se coloca en el lado de inicio de la celda (izquierda en un contenedor de izquierda a derecha, derecha en un contenedor de derecha a izquierda).
GridBagConstraints.CENTER: el componente se coloca en el centro de la celda.
GridBagConstraints.LINE_END (GridBagConstraints.EAST): el componente se coloca en el lado final de la celda (derecha en un contenedor de izquierda a derecha, izquierda en un contenedor de derecha a izquierda).
GridBagConstraints.LAST_LINE_START (GridBagConstraints.SOUTHWEST): el componente se coloca en la esquina inferior izquierda de la celda.
GridBagConstraints.PAGE_END (GridBagConstraints.SOUTH): el componente se coloca en la parte inferior de la celda.
GridBagConstraints.LAST_LINE_END (GridBagConstraints.SOUTHEAST): el componente se coloca en la esquina inferior derecha de la celda.
GridBagConstraints.BASELINE: el componente se coloca en la línea base de la celda.
GridBagConstraints.BASELINE_LEADING: el componente se coloca en la línea base de inicio de la celda.
GridBagConstraints.BASELINE_TRAILING: el componente se coloca en la línea base de final de la celda.
Conclusión
GridBagLayout es un gestor de diseño muy flexible que permite colocar componentes en una cuadrícula de filas y columnas. Al utilizar GridBagConstraints, se pueden controlar las propiedades de tamaño y posición de los componentes en la cuadrícula. Aunque GridBagLayout puede ser complejo de usar, ofrece una gran flexibilidad para diseñar interfaces de usuario complejas y personalizadas.
04. Patrones de diseño
En este apartado, repasaremos los principios de diseño de clases Java y los principales patrones de diseño empleados en el acceso a datos, sobre todo los patrones de creación.
Patrones de diseño
Un patrón de diseño es una solución general establecida para un problema común en el desarrollo de software. El propósito de un patrón de diseño es buscar estrategias comunes para aprovechar el conocimiento y la experiencia acumulada de los desarrolladores para resolver problemas fácilmente. También proporciona a los desarrolladores un vocabulario común en el que pueden discutir problemas y soluciones comunes. Por ejemplo, si dices que escribiste getters/setters o implementaste el patrón singleton, la mayoría de los desarrolladores entenderán la estructura de tu código sin tener que profundizar en los detalles de bajo nivel.
Los patrones que pueden resultar más interesantes para acceso a datos, son los patrones de creación, un tipo de patrón que gestiona la creación de objetos dentro de una aplicación.
Escritor escritor =new Poeta();
EL problema con la creación de objetos radica en cómo crear y gestionar objetos en sistemas más complejos. Por ejemplo, necesitamos saber exactamente qué tipo de objeto es Escritor, en este caso, Poeta, que se crea en tiempo de compilación. Pero, en muchos casos no se sabe hasta tiempo de ejecución, además de crear un solo objeto Escritor en la memoria compartido por todas las clases dentro de nuestra aplicación (patrón Singleton).
Los patrones de creación simplemente aplican un nivel de indirección a la creación de objetos al crear el objeto en alguna otra clase, en lugar de crear el objeto directamente en tu aplicación. El nivel de indirección es un término general para resolver un problema de diseño de software dividiendo conceptualmente la tarea en múltiples niveles.
Haremos un inciso para repasar y descubrir nuevos principios de diseño, pues son un elemento clave en el diseño de estructura de clases de acceso a Datos, patrones como Singleton son interesantes para establecer conexiones a BD, por ejemplo.
Un principio de diseño es una idea establecida o mejor práctica que facilita el proceso de diseño de software.
Al crear clases en Java estos principios conducen a bases de código mejores y más manejables. En general, seguir buenos principios de diseño lleva a:
Código más lógico
Código más fácil de entender
Clases más fáciles de reutilizar en otras relaciones y aplicaciones
Código más fácil de mantener y que se adapta más fácilmente a cambios en los requisitos de la aplicación
Un modelo de datos es la representación de nuestros objetos y sus propiedades dentro de la aplicación y cómo se relacionan con elementos del mundo real.
Encapsulación de Datos
Un principio fundamental del diseño orientado a objetos es el concepto de encapsular datos.
En el desarrollo de software, la encapsulación es declarar que los atributos y métodos en una clase de manera que los métodos operen sobre los atributos, en lugar de que los usuarios de la clase accedan directamente a los atributos. En Java, de manera general se implementan con atributos de instancia privados que tienen métodos públicos para recuperar o modificar los datos, comúnmente conocidos como getters y setters, respectivamente.
Ningún elemento además de la propia clase debería tener acceso directo a sus datos.
Se dice que la clase encapsula los datos que contiene y evita que alguien acceda directamente a ellos. Con la encapsulación, una clase puede mantener ciertas invariantes sobre sus datos internos. Una invariante es una propiedad o verdad que se mantiene incluso después de que los datos son modificados. Por ejemplo, imaginemos que estamos diseñando una nueva clase Animal y tenemos los siguientes requisitos de diseño:
Cada animal tiene un campo de especie no nulo y no vacío.
Cada animal tiene un campo de edad que es mayor o igual a cero.
El objetivo al diseñar nuestra clase Animal sería asegurarnos de que nunca lleguemos a una instancia de Animal que viole una de estas propiedades. Al usar miembros de instancia privados junto con métodos getter y setter que validan los datos de entrada, podemos garantizar que estas invariantes sigan siendo verdaderas.
Por ejemplo, si definimos la clase Animal sin encapsulación:
publicclassAnimal {
public String especie;
publicint edad;
}
Al definir la clase Animal de esta manera, es fácil crear una instancia de Animal que no cumpla las invariantes:
Animal animal =new Animal(); // La especie no debería ser nula.animal.edad=-80; // La edad no podría ser negativa.
L primera invariante se viola tan pronto como se crea el objeto, con la especie predeterminada a nula. Luego, el usuario establece el campo de edad en -80, ya que este campo es accesible públicamente, lo que resulta en la violación de la segunda invariante.
HAciendo los atributos privados, la clase es la única que puede modificar los datos directamente. Al definir constructores, getters y setters que cumplan las condiciones:
publicclassAnimal {
private String especie;
privateint edad;
publicAnimal(String especie) {
this.setEspecie(especie);
}
public String getEspecie() {
return especie;
}
publicvoidsetEspecie(String especie) {
if(especie ==null|| especie.trim().length()==0) {
thrownew IllegalArgumentException("La especie es obligatoria");
}
this.especie= especie;
}
publicintgetAge() {
return edad;
}
publicvoidsetAge(int edad) {
if(edad < 0) {
thrownew IllegalArgumentException("La edad no puede ser un número negativo");
}
this.edad= edad;
}
}
Las variables especie y edad están marcadas como privadas, con métodos públicos getEspecie() y getEdad() para leer los datos. Además, setEspecie() y setEdad() ahora validan la entrada y lanzan una excepción si se viola una de nuestras invariantes. Además, se ha añadido un constructor no predeterminado que requiere un valor de especie y utiliza el método setter para validar la entrada.
La ventaja de esta nueva implementación de la clase Animal es que utiliza la encapsulación para hacer cumplir los principios de diseño de la clase. Cada vez que se pasa una instancia de un objeto Animal a un método, se puede usar sin requerir que se validen sus invariantes.
Bloqueo de acceso directo a atributos privados
EN la práctica, los getter o setter a menudo ofrece un acceso casi directo a los atributos privados:
Aunque puede parecer una mala encapsulación, el campo nombre se puede cambiar sin aplicar ninguna regla, es mucho mejor que permitir el acceso directo a la variable privada nombre. La ventaja radica en que fácilmente se puede actualizar el método getter o setter para tener reglas más complejas sin hacer que las otras clases que lo usan tengan que recompilar el código. El método setNombre() podría reescribirse de la siguiente manera:
Dado que la firma del método setNombre() no cambió, la invocación de este método no implica tener que modificar y recompilar su código.
Se considera una buena práctica de diseño encapsular siempre todas las variables en una clase, incluso si no hay reglas de datos establecidas, como una forma de proteger los datos cuando dichas reglas puedan añadirse en el futuro.
Creación de JavaBeans
La encapsulación es tan prevalente en Java que existe un estándar para crear clases que almacenan datos, llamado JavaBeans. Un JavaBean es un principio de diseño para encapsular datos en un objeto en Java;
Convenciones de nomenclatura de JavaBean:
Regla
Ejemplo
Las propiedades son privadas.
private int edad;
El getter para propiedades no booleanas comienza con get.
public int getEdad() { return edad; }
Los getters para propiedades booleanas pueden comenzar con is, has o get.
public boolean isAve() { return ave; } public boolean getAve() { return ave; }
Los métodos setters comienzan con set.
public void setEdad(int edad) { this.edad = edad; }
El nombre del método debe tener un prefijo de set/get/is/has, seguido de la primera letra de la propiedad en mayúscula y el resto del nombre de la propiedad.
public void setNumHijos(int numHijos) { this.numHijos = numHijos; }
Aunque los valores booleanos utilizan is para comenzar sus métodos getters, NO se aplica a las instancias de la clase envolvente Boolean, que utilizan get.
Por ejemplo:
privateboolean jugando;
private Boolean bailando;
¿Cuál de las siguientes podría incluirse correctamente en un JavaBean?
La primera línea es correcta porque define un getter adecuado para una variable booleana. El segundo ejemplo también es correcto, ya que boolean puede usar is o get. La tercera línea es incorrecta, porque un envoltorio Boolean debería comenzar con get, ya que es un objeto.
public String nombre;
public String nombre() { return nombre; }
publicvoidactualizarNombre(String n) { nombre = n; }
publicvoidsetnombre(String n) { nombre = n; }
¡Ninguna de estas líneas sigue las prácticas correctas de JavaBean! La primera línea hace público el nombre, cuando debería ser privado. La segunda línea no define un getter adecuado y debería ser getNombre(). Las dos últimas líneas son incorrectos setters, ya que la primera no comienza con set y la segunda no tiene la primera letra del nombre del atributo en mayúscula.
Relación Es‐un
El operador instanceof se puede usar para determinar cuándo un objeto es una instancia de una clase, superclase o interfaz particular. En el diseño orientado a objetos, se describe la relación de que un objeto es una instancia de un tipo de datos como tener una relación es‐un. La relación es‐un también se conoce como la prueba de herencia.
Cuando se construye un modelo de datos basado en la herencia, es importante aplicar la relación es‐un regularmente, para diseñar clases que conceptualmente tengan sentido. Por ejemplo, una clase Gato que hereda de una clase Mascota, como se muestra en la imagen:
classDiagram
Mascota <|-- Gato
La clase principal, Mascota, tiene campos comunes como nombre y edad. Se podría incurrir en el error de diseñar una clase Tigre, y dado que los tigres también tienen una edad y un nombre, se podría estar tentado en reutilizar la clase principal Mascota con el propósito de ahorrar tiempo y líneas de código, dando lugar a diseños incorrectos:
classDiagram
Mascota <|-- Gato
Mascota <|-- Tigre
Por desgracias ;-), Mascota también tiene un método acariciar().
Al reutilizar la clase principal Mascota, se está afirmando conceptualmente que un Tigre es‐una Mascota, pero un Tigre no es una Mascota. Aunque este ejemplo es funcionalmente correcto y ahorra tiempo y líneas de código, el resultado de no aplicar la relación es‐un es que se ha creado una relación que viola el modelo de datos. Intentemos solucionar el problema colocando Mascota y Tigre debajo de una clase padre Felino y veamos si eso resuelve el problema:
La estructura de clases es ahora más coherente, pero si se añade un hijo Perro a Mascota, nos encontramos con un problema con la prueba es‐un. Un Perro es‐una Mascota, y una Mascota es‐una Felino, pero el modelo implica que un Perro es‐un Felino, lo cual obviamente no es cierto.
La prueba de relación es‐un ayuda a evitar crear modelos de objetos que contengan contradicciones. Una solución en este ejemplo sería no combinar Tigre y Mascota en el mismo modelo, prefiriendo escribir código duplicado en lugar de crear datos inconsistentes. Una mejor solución podría ser utilizar las propiedades de las interfaces y declarar Mascota como una interfaz en lugar de una clase principal:
Ahora es correcto usando la prueba es‐un: Gato es‐un Animal, Tigre es‐un Felino, Perro es‐un Animal, y así sucesivamente. Mascota ahora está separada del modelo de herencia de clases, pero al usar interfaces, preservamos la relación de que Gato es‐una Mascota y Perro es‐una Mascota.
Relación Tiene‐un
En el diseño orientado a objetos, a menudo queremos probar si un objeto contiene una propiedad o valor en particular. La relación tiene‐un cuando la propiedad de una clase tiene un nombre de un objeto o primitiva como miembro. La relación tiene‐un también se conoce como la prueba de composición de objetos.
Por ejemplo, las clases Pájaro y Pico:
classDiagram
Pato o-- Pico
class Pico {
-String color
-double longitud
}
class Pato{
-Pico pico
-Pie pieDerecho
-Pie pieIzquierdo
}
Pájaro y Pico son clases con atributos y valores diferentes. Aunque obviamente fallan la prueba es‐un, ya que un Pájaro no es un Pico, ni un Pico es un Pájaro, sí pasan la prueba tiene‐un, ya que un Pájaro tiene un Pico.
La herencia va un paso más allá al permitirnos decir que cualquier hijo de Pájaro también debe tener un Pico.
Problemas del modelo de datos empleando Es‐un y Tiene‐un
A veces, las relaciones parecen pasar la prueba es‐un pero fallan al combinarse con la relación tiene‐un a través de la herencia. Por ejemplo:
publicclassCola {}
publicclassPrimate {
protected Cola cola;
}
publicclassMonoextends Primate { // El mono tiene una cola ya que es un primate}
publicclassChimpanceextends Primate { // El chimpancé tiene una cola ya que es un primate}
En el ejemplo, un Mono es‐un Primate y un Chimpance es‐un Primate. El modelo también establece que un Primate tiene‐una Cola, y mediante la herencia, un Mono tiene‐un Cola y un Chimpancé tiene‐un Cola. Sin embargo, los chimpancés no tienen cola, por lo que el modelo de datos subyacente es incorrecto.
Deberíamos eliminar la propiedad Cola de la clase Primates, ya que no todos los primates tienen colas.
Composición de Objetos
En el diseño orientado a objetos la composición de objetos es la propiedad de construir una clase utilizando referencias a otras clases para reutilizar la funcionalidad de esas clases.
La clase contiene las otras clases en el sentido de tiene‐un y puede delegar métodos a las otras clases.
La composición de objetos debe considerarse como una alternativa a la herencia y a menudo se utiliza para simular un comportamiento polimórfico que no se puede lograr mediante la herencia simple:
publicclassAletas {
publicvoidaletear() {
System.out.println("Las aletas se mueven de un lado a otro");
}
}
publicclassPatasPalmeadas {
publicvoidpatear() {
System.out.println("Las patas palmeadas patean de un lado a otro");
}
}
Intentar relacionar estos objetos mediante la herencia no tiene sentido, ya que las PatasPalmeadas no son lo mismo que las Aletas. En cambio, podemos crear una nueva clase que contenga ambas de estas clases y delegue sus métodos en ella:
La clase Pingüino está compuesta por instancias de Aletas y PatasPalmeadas. La parte difícil de aletear() y patear() se delega a las otras clases, siendo los métodos en la clase Pingüino de solo una línea. Ten en cuenta que las implementaciones de estos métodos en las clases delegadas también tienen solo una línea, aunque podrían ser considerablemente más complejas.
Una de las ventajas de la composición de objetos sobre la herencia es que tiende a promover una mayor reutilización de código. Al utilizar la composición de objetos, se accede a otras clases y métodos que serían difíciles de obtener mediante el modelo de herencia simple.
En el ejemplo, la clase Aletas se puede reutilizar en clases completamente no relacionadas con un Pingüino o un Ave, como en una clase Delfín o Tortuga. Si la clase Aletas hubiera sido heredada de la clase Pingüino, entonces usarla en otras clases no relacionadas sería difícil sin romper el modelo de clases o tener que hacer que la otra clase contenga una instancia de un Pingüino. Por ejemplo, sería absurdo decir que un Delfín hereda de un Pingüino o tiene una instancia de una clase Pingüino, sólo porque un Delfín tiene Aletas y Aletas hereda de la clase Pingüino.
Por otro lado, sobrecarga de métodos para determinar dinámicamente qué método seleccionar en tiempo de ejecución es una herramienta muy útil.
En muchos casos, precisamos crear un objeto en memoria sólo una vez en una aplicación y compartirlo entre varias clases. Un ejemplo podría ser una conexión a una base de datos, pero sucede con frecuenta con otro tipo de recursos compartidos.
Por ejemplo, podríamos querer gestionar la cantidad de comida disponible para la alimentación de los animales de un zoológico en todas las clases que lo utilizan. Podríamos pasar el mismo objeto compartido ComidaManager a cada clase y método que lo utiliza, aunque esto crearía muchos punteros adicionales y podría ser difícil de gestionar si el objeto se utiliza en toda la aplicación. Al crear un objeto singleton ComidaManager, centralizamos los datos y eliminamos la necesidad de pasarlos por toda la aplicación.
El patrón singleton es un patrón de creación que permite sólo una instancia de un objeto en memoria dentro de una aplicación, compartida por todas las clases y hilos dentro de la aplicación.
El objeto disponible globalmente creado por el patrón singleton se denomina objeto singleton.
Los singleton también pueden mejorar el rendimiento cargando datos reutilizables que de otro modo serían costosos de almacenar y recargar cada vez que se necesitan.
Una implementación sencilla de clase ComidaManager con un patrón singleton básico:
publicclassAlmacenComida {
privateint cantidad = 0;
privateAlmacenComida() {} // Privado, se crea como atributo estático:privatestaticfinal AlmacenComida instancia =new AlmacenComida(); // static: único.// Método público único para obtener la instancia:publicstatic AlmacenComida getInstance() {
return instancia;
}
publicsynchronizedvoidaddComida(int cantidad) {
this.cantidad+= cantidad;
}
publicsynchronizedbooleanremoveComida(int cantidad) {
if (this.cantidad< cantidad) returnfalse;
this.cantidad-= cantidad;
returntrue;
}
publicsynchronizedintgetCantidadComida() {
return cantidad;
}
}
Los singleton en Java se crean como atributos privados estáticos dentro de la clase, a menudo con el nombre instance (o instancia).
Se accede a ellos a través de un único método público estático, a menudo llamado getInstance(), que devuelve la referencia al objeto singleton.
Todos los constructores en una clase singleton se declaran como privados, lo que garantiza que ninguna otra clase pueda instanciar otra versión de la clase. Al marcar los constructores como privados, hemos marcado implícitamente la clase como final.
Recuerda que cada clase requiere al menos un constructor, con el constructor por defecto si no se proporciona ninguno. Además, la primera línea de cualquier constructor es una llamada a un constructor padre super(). Si todos los constructores se declaran privados en la clase singleton, entonces es imposible crear una subclase con un constructor válido; por lo tanto, la clase singleton es final.
Se añade el modificador synchronized a addComida(), removeComida() y getCantidadComida(), para evitar que dos procesos ejecuten el mismo método exactamente al mismo tiempo y haya inconsistencias.
En el ejemplo AlmacenComida, un proceso que quiera usar este singleton primero llama a getInstance() y luego llama al método público necesario:
publicclassEntrenadorLlamas {
publicbooleanalimentarLlamas(int numeroLlamas) {
int cantidadNecesaria = 5 * numeroLlamas;
AlmacenComida AlmacenComida = AlmacenComida.getInstance();
if (AlmacenComida.getCantidadComida() < cantidadNecesaria) {
AlmacenComida.addComida(cantidadNecesaria + 10);
}
boolean alimentadas = AlmacenComida.removeComida(cantidadNecesaria);
if (alimentadas)
System.out.println("Las llamas han sido alimentadas");
return alimentadas;
}
}
Un aspecto a tener en cuenta es que puede haber varias clases en la aplicación que requieran el objeto Singleton.
En el ejemplo, equivaldría a muchas instancias de EntrenadorLlamas pero sólo una instancia del objeto Singleton, AlmacenComida.
El boolean devuelto por removeComida(), permite comprobar si hay disponible.
En nuestro primer ejemplo de AlmacenComida, instanciamos el objeto singleton directamente en la definición de la referencia de instancia.
Tanto la clase RegistroPersonal como la clase anterior AlmacenComida instancian el singleton en el momento en que se carga la clase. Sin embargo, a diferencia de la clase AlmacenComida, la clase RegistroPersonal instancía el singleton como parte de un bloque de inicialización estático.
Aunque estas dos implementaciones son equivalentes, ya que ambas crean el singleton cuando se carga la clase, el bloque de inicialización estático permite realizar pasos adicionales para configurar el singleton después de que se ha creado. También permite gestionar casos en los que el constructor de RegistroPersonal lanza una excepción.
Dado que el singleton se crea cuando se carga la clase, permite marcar la como final, lo que garantiza que sólo se creará una instancia dentro de la aplicación.
Cuándo usar singleton
Los singletons se utilizan en situaciones donde necesitamos acceso a un conjunto único de datos en toda una aplicación. Por ejemplo: datos de configuración de la aplicación o las cachés de datos reutilizables se implementan comúnmente mediante singletons. Los singletons también se pueden utilizar para coordinar el acceso a recursos compartidos, como coordinar el acceso de escritura a un archivo o acceso a bases de datos.
Hay muchas formas de hacer esto en Java. Todas estas formas difieren en su implementación del patrón, pero al final, todas logran el mismo resultado final de una única instancia.
1. Inicialización en la decleración
Es el método más sencillo de crear una clase singleton. El objeto de la clase se crea cuando se carga en la memoria por la JVM, asignando directamente la referencia de una instancia.
Puede utilizarse cuando el programa siempre usará una instancia de esta clase, o el costo de crear la instancia no es demasiado grande en términos de recursos y tiempo.
// Código Java para crear una clase singleton mediante// "Inicialización Ansiosa"publicclassClaseSingletonAD {
// instancia pública inicializada al cargar la claseprivatestaticfinal ClaseSingletonAD instance =new ClaseSingletonAD();
privateClaseSingletonAD() {
// constructor privado }
publicstatic ClaseSingletonAD getInstance(){
return instance;
}
}
Ventajas/inconvenientes:
Muy sencilla de implementar.
Puede llevar al desperdicio de recursos, ya que la instancia de la clase se crea siempre, ya sea que se necesite o no.
También se pierde tiempo de CPU en la creación de la instancia si no es necesaria.
No es posible manejar excepciones.
2. Usando bloques static
Es muy similar al caso anterior. La única diferencia es que el objeto se crea en un bloque estático para que se pueda tener acceso a su creación, como el manejo de excepciones. De esta manera, el objeto también se crea en el momento de la carga de la clase.
Puede utilizarse cuando hay posibilidad de excepciones al crear el objeto con inicialización ansiosa.
// Código Java para crear una clase singleton// Usando bloque estáticopublicclassClaseSingletonAD {
// instancia públicapublicstatic ClaseSingletonAD instance;
privateClaseSingletonAD() {
// constructor privado }
static {
// bloque estático para inicializar la instancia instance =new ClaseSingletonAD();
}
}
Ventanjas e inconvenientes:
Muy sencillo de implementar.
No es necesario implementar el método getInstance(). La instancia se puede acceder directamente.
Las excepciones se pueden manejar en el bloque estático.
Puede llevar al desperdicio de recursos, ya que la instancia de la clase se crea siempre, ya sea que se necesite o no.
También se pierde tiempo de CPU en la creación de la instancia si no es necesaria.
3. Instanciación “perezosa” de Singleton
En este método, el objeto se crea sólo si es necesario. Esto puede evitar el desperdicio de recursos.
El objeto se crea en la implementación del método getInstance() que devuelva la instancia. Hay una comprobación de nulidad, y si el objeto no está creado, entonces se crea; de lo contrario, se devuelve el creado previamente.
Dado que el objeto se crea dentro de un método, se garantiza que el objeto no se creará a menos que sea necesario. La instancia se mantiene privada para que nadie pueda acceder directamente a ella.
Puede utilizarse en un entorno de un solo hilo porque varios hilos pueden romper la propiedad singleton al acceder al método getInstance() simultáneamente y crear varios objetos.
// inicialización perezosa (Lazy initialization)publicclassClaseSingletonAD {
// instancia privada// sólo accesible desde el méttodo getInstance()privatestatic ClaseSingletonAD instance;
privateClaseSingletonAD() {
// constructor privado }
//devuelve la instancia de la clasepublicstatic ClaseSingletonAD getInstance() {
if (instance ==null) {
// si la instancia es nula se inicializa instance =new ClaseSingletonAD();
}
return instance;
}
}
Por ejemplo:
// Instanciación perezosapublicclassRastreadorTicketsVisitantes {
privatestatic RastreadorTicketsVisitantes instancia;
privateRastreadorTicketsVisitantes() {
}
publicstatic RastreadorTicketsVisitantes getInstance() {
if (instancia ==null) {
instancia =new RastreadorTicketsVisitantes(); // ¡NO ES SEGURO PARA HILOS! }
return instancia;
}
// Métodos de acceso a datos// ...}
El RastreadorTicketsVisitantes, al igual que nuestras clases singleton, declara sólo constructores privados, crea una instancia de singleton y devuelve el singleton con un método getInstance(). Sin embargo, la clase RastreadorTicketsVisitantescrea el objeto singleton primera vez que un cliente lo solicita. Crear un objeto reutilizable la primera vez que se solicita es un patrón de diseño de software conocido como instanciación perezosa (Lazy Instantiation), que se usa a menudo junto con el patrón singleton.
Ventajas:
La instanciación perezosa reduce el uso de memoria y mejora el rendimiento cuando se inicia una aplicación. De hecho, sin instanciación perezosa, la mayoría de los sistemas operativos y aplicaciones que ejecutas tardarían significativamente más en cargarse y consumirían mucha más memoria, quizás más memoria de la disponible en tu computadora.
Inconveniente:"
El inconveniente de la instanciación perezosa es que los usuarios pueden notar un retraso notable la primera vez que se necesita un tipo particular de recurso, como una conexión a una base de datos.
Por ejemplo, muchas herramientas libres IDEs de desarrollo, como Eclipse, a menudo muestran un ligero retraso la primera vez que se abre un archivo Java en una ventana del editor después de iniciar el programa. Sin embargo, el retraso desaparece cuando se abren archivos Java adicionales. Este es un ejemplo de instanciación perezosa, ya que Eclipse sólo carga las bibliotecas para analizar y presentar archivos Java la primera vez que se abre un archivo Java.
El objeto se crea sólo si es necesario. Puede superar el desperdicio de recursos y tiempo de CPU.
El manejo de excepciones también es posible en el método.
Se debe verificar la condición de nulo cada vez.
La instancia no se puede acceder directamente.
En un entorno multithread, puede romper la propiedad singleton.
Singleton en varias computadoras
En principio, los singletons son siempre únicos. Cuando se escriben aplicaciones que se ejecutan en varias computadoras, la solución estática de singleton comienza a requerir consideraciones especiales, ya que cada computadora tendría su propio JVM. En esas situaciones, aún se podría usar el patrón singleton, aunque podría implementarse con una base de datos o un servidor de colas en lugar de como un objeto estático.
4. Thread Safe Singleton
Implementar verdaderamente el patrón singleton, debemos asegurarnos de que sólo se cree una instancia del singleton y está garantizado con el modelo anterior en entornos con un úncio hilo de ejecución.
Marcar el constructor como privado evita que el singleton sea creado por otras clases, pero también se necesita asegurar que el objeto singleton sólo se crea una vez dentro de la propia clase singleton.
Se garantiza en los ejemplos de las clases AlmacenComida y RegistroPersonal utilizando el modificador final en la referencia estática.
Pero, en la instanciación perezosa en la clase RastreadorTicketsVisitantes, el compilador no permite asignar el modificador final a la referencia estática.
La implementación de RastreadorTicketsVisitantes, como se muestra, no se considera segura para hilos de ejecución (Threads), ya que dos hilos podrían llamar a getInstance() al mismo tiempo, lo que daría lugar a la creación dos objetos. Después de que ambos hilos terminen de ejecutarse, sólo se establecerá y utilizará un objeto por otros hilos en adelante, pero el objeto que recibieron los dos hilos iniciales puede no ser el mismo.
La seguridad de hilos (Thread safety) es la propiedad de un objeto que **garantiza una ejecución segura por parte de múltiples hilos al mismo tiempo mediante el uso del modificador synchronized** (por ejemplo, es la diferencia entre StringBuilder y StringBuffer). Hace que el método getInstance() sea sincronizado para que múltiples hilos no puedan acceder a él simultáneamente.
El método getInstance() ahora está sincronizado, lo que significa que sólo se permitirá que un hilo entre en el método a la vez, asegurando que sólo se cree un objeto.
// Programa Java para crear una clase Singleton// segura para subprocesospublicclassClaseSingletonAD{
// instancia privada, para que sólo pueda ser// accedida por el método getInstance()privatestatic ClaseSingletonAD instance;
privateClaseSingletonAD() {
// constructor privado }
// método sincronizado para controlar el acceso simultáneosynchronizedpublicstatic ClaseSingletonAD getInstance() {
if (instance ==null) {
// si la instancia es nula, inicializar instance =new ClaseSingletonAD();
}
return instance;
}
}
Ventajas e inconvenientes:
La inicialización perezosa se implanta.
También es seguro para threads.
El método getInstance() está sincronizado, por lo que provoca un rendimiento lento, ya que varios hilos no pueden acceder a él simultáneamente.
5. Singletons con bloqueo de doble comprobación (Double‐Checked Locking)
La implementación sincronizada de getInstance(), aunque evita correctamente la creación de varios objetos singleton, tiene el problema de que cada llamada a este método requerirá sincronización. En la práctica, esto puede ser costoso y afectar el rendimiento.
La sincronización sólo es necesaria la primera vez que se crea el objeto. La solución es utilizar el doble bloqueo, un patrón de diseño en el que primero probamos si se necesita sincronización antes de adquirir cualquier bloqueo:
Se añade el modificador volatile al objeto singleton. Esta palabra clave evita un caso sutil donde el compilador intenta optimizar el código de manera que el objeto se acceda antes de que termine de construirse.
Esta solución es mejor que la versión anterior, ya que realiza el paso de sincronización sólo cuando el singleton no existe. El singleton se accede miles de veces durante muchas horas o días, esto significa que sólo las primeras llamadas requerirían sincronización, y el resto no.
Realiza inicialización perezosa.
Es thread-safe
Mejora el rendimiento, porque reduce la sincronización.
La primera vez puede afectar al rendimiento.
Conclusión
Singleton es un patrón de diseño útil para permitir sólo una instancia de su clase, pero los errores comunes pueden permitir que se cree más de una instancia sin darse cuenta.
El propósito del Singleton es controlar la creación de objetos, limitando el número a uno pero permitiendo la flexibilidad de crear más objetos si la situación cambia. Dado que sólo hay una instancia de Singleton, cualquier campo de instancia de un Singleton aparecerá sólo una vez por clase, al igual que los campos estáticos.
Los singleton a menudo controlan el acceso a recursos como conexiones de bases de datos o sockets. Por ejemplo, si se tiene una licencia para una sola conexión para su base de datos o su controlador JDBC tiene problemas con subprocesos múltiples, Singleton se asegura de que sólo se realice una conexión o que sólo un subproceso pueda acceder a la conexión a la vez. Si añaden conexiones de base de datos o se utiliza un controlador JDBC que permite subprocesos múltiples, Singleton se puede ajustar fácilmente para permitir más conexiones.
Además, los Singleton pueden tener estado; en este caso, su función es servir como depósito único del Estado. Si está implementando un contador que necesita dar números secuenciales y únicos (como la máquina que da números en la tienda de delicatessen), el contador debe ser globalmente único. El Singleton puede retener el número y sincronizar el acceso; Si más adelante desea mantener contadores en una base de datos para lograr persistencia, puede cambiar la implementación privada de Singleton sin cambiar la interfaz.
Por otro lado, los Singleton también pueden proporcionar funciones de utilidad que no necesitan más información que sus parámetros. En ese caso, no hay necesidad de crear instancias de múltiples objetos que no tienen ninguna razón para su existencia, por lo que un Singleton es apropiado.
El próximo patrón creacional que discutiremos es el patrón de objetos inmutables, empelado para cuando deseamos tener objetos de sólo lectura para que sean compartidos y utilizados por varias clases.
A veces queremos crear objetos simples que puedan ser compartidos entre varias clases, pero por razones de seguridad no queremos que su valor sea modificado. Podríamos copiar el objeto antes de enviarlo a otro método, pero esto crea una gran sobrecarga que duplica el objeto cada vez que se pasa. Además, si tenemos varios hilos que acceden al mismo objeto, podríamos tener problemas de concurrencia, como veremos en el Capítulo 7.
Solución:
El patrón de objeto inmutable es un patrón creacional basado en la idea de crear objetos cuyo estado no cambia después de que se crean y que se pueden compartir fácilmente entre varias clases. Los objetos inmutables van de la mano con la encapsulación, excepto que no existen métodos setter que modifiquen el objeto. Dado que el estado de un objeto inmutable nunca cambia, son inherentemente seguros para subprocesos.
Aplicación de una Estrategia Inmutable
Aunque hay una variedad de técnicas para escribir una clase inmutable, debes estar familiarizado con una estrategia común para hacer que una clase sea inmutable para el examen:
Usa un constructor para establecer todas las propiedades del objeto.
Marca todas las variables de instancia como privadas y finales.
No definas ningún método setter.
No permitas que los objetos mutables referenciados se modifiquen o accedan directamente.
Evita que los métodos se anulen.
La primera regla define cómo creamos el objeto inmutable, pasando la información al constructor para que todos los datos se establezcan al crearlo. Las segundas y terceras reglas son directas, ya que provienen de la encapsulación adecuada. Si las variables de instancia son privadas y finales, y no hay métodos setter, entonces no hay una forma directa de cambiar la propiedad de un objeto. Todas las referencias y valores primitivos contenidos en el objeto se establecen en la creación y no se pueden modificar.
La cuarta regla requiere una explicación más detallada. Supongamos que tienes un objeto inmutable Animal, que contiene una referencia a una lista de alimentos favoritos del animal, como se muestra en el siguiente ejemplo:
import java.util.*;
publicfinalclassAnimal {
privatefinal List<String> favoriteFoods;
publicAnimal(List<String> favoriteFoods) {
if (favoriteFoods ==null) {
thrownew RuntimeException("favoriteFoods is required");
}
this.favoriteFoods=new ArrayList<String>(favoriteFoods);
}
public List<String>getFavoriteFoods() { // ¡HACE LA CLASE MUTABLE!return favoriteFoods;
}
}
Para asegurarnos de que la lista favoriteFoods no sea nula, la validamos en el constructor y lanzamos una excepción si no se proporciona. El problema en este ejemplo es que el usuario tiene acceso directo a la lista definida en nuestra instancia de Animal. Aunque no pueden cambiar el objeto List al que apunta, pueden modificar los elementos de la lista, por ejemplo, eliminando todos los elementos llamando a getFavoriteFoods().clear(). También podrían reemplazar, eliminar o incluso ordenar la lista.
La solución, entonces, es nunca devolver esa referencia de lista al usuario. De manera más general, nunca deb
es compartir referencias a un objeto mutable contenido dentro de un objeto inmutable. Si el usuario necesita acceso a los datos en la lista, ya sea creando métodos envolventes para iterar sobre los datos o creando una copia única de los datos que se devuelve al usuario y nunca se almacena como parte del objeto. De hecho, la API de Collections incluye el método Collections.unmodifiableList(), que hace exactamente esto.
La clave aquí es que ninguno de los métodos que creas debe modificar el objeto mutable.
Volviendo a nuestras cinco reglas, la última regla es importante porque evita que alguien cree una subclase de tu clase en la que un valor previamente inmutable ahora parece mutable. Por ejemplo, podrían anular un método que modifica una variable diferente en la subclase, ocultando essencialmente la variable privada definida en la clase principal. La solución más simple es marcar la clase o los métodos con el modificador final, aunque esto limita el uso de la clase. Otra opción es hacer que el constructor sea privado y aplicar el patrón de fábrica, que discutiremos más adelante en este capítulo.
¿Sigue este ejemplo las cinco reglas? Bueno, todos los campos están marcados como privados y finales, y el constructor los establece al crear el objeto. Luego, no hay métodos setter y la clase en sí está marcada como final, por lo que los métodos no pueden ser anulados por una subclase. La clase contiene un objeto mutable, List, pero no hay referencias al objeto disponibles públicamente. Proporcionamos dos métodos para recuperar el número total de alimentos favoritos, así como un método para recuperar un alimento basado en un valor de índice. Ten en cuenta que String se considera inmutable, por lo que no tenemos que preocuparnos de que los objetos String se modifiquen. Por lo tanto, se cumplen las cinco reglas y las instancias de esta clase son inmutables.
Manejo de Objetos Mutables en los Constructores de Objetos Inmutables
Puedes notar que creamos un nuevo ArrayList en el constructor de Animal. Esto es absolutamente importante para evitar que la clase que crea inicialmente el objeto mantenga una referencia a la List mutable utilizada por Animal. Considera si hubiéramos hecho lo siguiente en el constructor:
this.favoriteFoods= favoriteFoods;
Con este cambio, el llamador que crea el objeto está utilizando la misma referencia que el objeto inmutable, lo que significa que ¡tiene la capacidad de cambiar la List! Es importante, al crear objetos inmutables, copiar cualquier argumento de entrada mutable a la instancia en lugar de usarlo directamente.
“Modificación” de un Objeto Inmutable
¿Cómo modificamos objetos inmutables si son inherentemente inmodificables? La respuesta es que ¡no podemos! Alternativamente, podemos crear nuevos objetos inmutables que contengan toda la misma información que el objeto original más lo que queríamos cambiar. Esto sucede cada vez que combinamos dos cadenas:
En este ejemplo, firstName es inmutable y no se modifica al agregarse a fullName, que también es un objeto inmutable. También podemos hacer lo mismo con nuestra clase Animal. Imagina que queremos aumentar la edad de un Animal en uno. Lo siguiente crea dos instancias de Animal, la segunda utilizando una copia de los datos de la primera instancia:
// Crea una nueva instancia de AnimalAnimal leon =new Animal("león", 5, Arrays.asList("carne", "más carne"));
// Crea una nueva instancia de Animal usando datos de la primera instanciaList<String> alimentosFavoritos =new ArrayList<String>();
for (int i = 0; i < leon.getFavoriteFoodsCount(); i++) {
alimentosFavoritos.add(leon.getFavoriteFood(i));
}
Animal leonActualizado =new Animal(leon.getSpecies(), leon.getAge() + 1, alimentosFavoritos);
Dado que no teníamos acceso directo a la List mutable favoriteFoods, tuvimos que copiarla utilizando los métodos disponibles en la clase inmutable. También podríamos simplificar esto definiendo un método en Animal que devuelva una copia de la List de alimentos favoritos, siempre que el llamador comprenda que modificar esta List copiada no cambia el objeto Animal original de ninguna manera.
05. Programación en Kotlin
Abordaremos contenidos básicos de la programación estructurada, orientada a objetos y funcional. No son contenidos muy detallados, sólo se alcanza el nivel de detalle suficiente para abordar los primeros retos algorítmicos.
Subsecciones de 05. Programación en Kotlin
Programación estructurada básica
Abordaremos los contenidos más básicos de programación, que se corresponden con lo que se conoce como “programación estructurada” utilizando el lenguaje de programación Kotlin. No son contenidos muy detallados, sólo se alcanza el nivel de detalle suficiente para abordar los primeros retos algorítmicos.
Subsecciones de Programación estructurada básica
La funcion main()
Qué es una función
Una función se puede definir informalmente como un bloque de código diseñado para realizar una tarea en particular. En Kotlin todas las funciones comienzan a escribirse con la palabra reservada fun
La función main()
Toda aplicación necesita un punto de entrada, es decir, un sitio por de comenzar la ejecución. Para nuestras aplicaciones de consola el punto de entrada será la función main(). Todos nuestros programas tiene que tener escrita obligatoriamente esta función.
Nuestro primer ejemplo:
fun main() {
println("hola mundo!")
}
Intuye la forma de escribir una función en Kotlin:
comienza con la palabra reservada fun
le sigue el nombre de la función, main en este caso
paréntesis que pueden estar vacios o con parámetros como veremos a continuación
un cuerpo de la función que va entre las llaves {}
dentro de las llaves irán las instrucciones o código de la función. en este otra función que imprime un mensaje
La función main() con parámetros
La función main() a menudo la veremos escrita con parámetros. El funcionamiento de los parámetros es bastante similar a los parámetros de una función matemática. Iremos viendo el funcionamiento de los parámetros en las funciones poco a poco.
fun main(args: Array<String>) {
println("hola mundo!")
}
Por el momento para nuestros primeros ejemplos sencillos no son necesarios parámetros y escribiremos la función main sin parámetros como en el primer ejemplo.
Sobre el término compilador
Verás este termino con precisión y formalidad más adelante. Es una palabra que se utiliza frecuentemente en las explicaciones para aprender a programar. Para salir del paso, decimos que el compilador es una herramienta(programa) de un lenguaje de programación que se encarga de revisar que las instrucciones que escribimos son correctas según las normas del lenguaje y a continuación lo traduce a un lenguaje de bajo nivel (0s y 1s) para ser ejecutado en el procesador.
Frecuentemente diremos que algo “da error de compilación” para indicar que algo está mal escrito y no vamos poder ejecutar el programa hasta que corrigamos el error.
En el siguiente ejemplo nos olvidamos del paréntesis de cierre. Cuando intentamos ejecutar el programa el compilador nos da un error y el programa no llega a ejecutarse.
fun main(args: Array<String>) {
println("hola mundo!"
}
La funcion print() y println()
Fíjate que ya usamos en nuestro primer ejemplo la función println(). A continuación aprenderemos alguna cosa más sobre esta imortante función y de paso sobre las funciones Kotlin en general.
El término salida estándar
El término salida se refiere al dispositivo en el que un programa escribe algo como por ejemplo un disco, una impresora o la pantalla del ordenador. El término salida estandar se refiere a la salida que el programa usa por defecto. Normalmente la salida estandar es la pantalla (monitor) así que a efectos prácticos, por el momento, puedes considerar monitor y salida estándar como sinónimos.
Argumentos de una función
Sin el menor rigor, teniendo en cuenta que lo explicaremos más adelante detalladamente intenta entender lo que es un argumento:
Cuando usas una función ya definida y la llamas o invocas lo que escribes entre paréntesis son los argumentos. Internamente, la función usará esos argumentos o valores para hacer algo.
Por ejemplo en
println("hola mundo")
“hola mundo” es un argumento. La función toma el argumento que le indicamos y es capaz de imprimir ese argumento en la pantalla.
Diferencia entre print() y println()
print() es una función en Kotlin que imprime su argumento en la salida estándar. De manera similar, println() es otra función que imprime su argumento en la salida estándar pero también agrega un salto de línea en la salida. Puedes probar el siguiente ejemplo para observar la diferencia
fun main(){
println("Hello,")
println(" world!")
print("Hello,")
print(" world!")
}
Al ejecutar generará el siguiente resultado
Hello,
world!
Hello, world!
Ambas funciones, print() y println(), se pueden usar para imprimir números y cadenas pero también se pueden pasar como argumentos expresiones que pueden consistir en un cálculo matemático o una concatenación de cadenas de caracteres entre otras muchas posibilidades.
El detalle de que expresiones admiten estas funciones se irá viendo poco a poco. Por el momento basta con que aprecies que a las indicaciones dadas.
Comentarios
Un comentario es una explicación que el programador deja entre el código que escribe. Dicho comentario a la persona que lee el el código pero es ignorado por el compilador. Se usan comentarios para:
dejar explicaciones extra para cuando otro programador lea el código
dejar cualquier aclaración o recordatorio para el propio autor del programa.
Al igual que la mayoría de los lenguajes modernos, Kotlin admite comentarios de una sola línea (o de final de línea) y de varias líneas (bloque).
Comentarios de una sola línea
Los comentarios de una sola línea en Kotlin comienzan con dos barras diagonales // y terminan con el final de la línea. Por lo tanto , el compilador de Kotlin ignora cualquier texto escrito entre // y el final de la línea.
El siguiente es el programa Kotlin de muestra que utiliza un comentario de una sola línea
// esto es un comentario
fun main() {
println("Hello, World!")
}
Cuando ejecute el programa Kotlin anterior, generará el siguiente resultado:
Hello, World!
Es decir, el comentario fue ignorado por el compilador. No tuvo ninguna influencia en la ejecución del programa.
Un comentario de una sola línea puede comenzar desde cualquier parte del programa y finalizará hasta el final de la línea. Por ejemplo, puede usar un comentario de una sola línea de la siguiente manera:
fun main() {
println("Hello, World!") // Esto también es un comentario
}
Comentarios multilínea
Comienza con /* y termina con */ . Por lo tanto, cualquier texto escrito entre /* y */ se tratará como un comentario y el compilador de Kotlin lo ignorará.
Los comentarios de varias líneas también se denominan comentarios de bloque en Kotlin.
El siguiente es el programa Kotlin de muestra que utiliza un comentario de varias líneas:
/* Esto es un comentario multiLinea
puede escribrise o extenderse
en tantas líneas como tu quieras
*/
fun main() {
println("Hello, World!")
}
Aunque no es necesario, es típico y una buena práctica, al escribir comentarios multilínea, añadir un * a cada linea que forma parte del comentario. Esto hace, especialmente en comentarios largos, que no confundamos lineas de comentario con lineas de código.
/* Esto es un comentario multiLinea
* puede escribrise o extenderse
* en tantas líneas como tu quieras
*/
fun main() {
println("Hello, World!")
}
Variables, literales y sentencia de asignación
De forma informal y como punto de partida, llamamos variables a las ubicaciones de la memoria del ordenador que se usan para almacenar valores en un programa. Luego, a lo largo del programa podemos usa esos nombres para recuperar los valores almacenados y usarlos en el programa.
Si tengo una variable x que almacena el valor 5 puedo decir indistintamente que x almacena 5 o simplemente que x vale 5.
A lo largo del curso afinaremos el concepto de variable pero con esta idea inicial informal es suficiente para despegar.
Crear variables
Hay varias posibilidades nos centramos en la más habitual.
Las variables de Kotlin se crean usando las palabras var o val y luego un signo igual = para asignar un valor a esas variables. En el siguiente ejemplo creamos una variable con var y otra con val
var nombre = "Juan Perro"
val edad = 15
Más adelante en este documento explicaremos su diferencia.
Sentencia de asignación
Una sentencia de asignación consiste en una sentencia con la siguiente estructura
nombreDeVar = valor o expresión
Cuando declaramos en los ejemplos anteriores una variable estabamos realmente usando una sentencia de asignación despues de la palabra reservada var.
Una vez que creo una variable, si es mutable (concepto que vemos más adelante), puedo modificar su valor con todas las instrucciones de asignación que quiera.
fun main() {
var numero = 3
println(numero)
numero = 7 //ahora la variable número almacena el valor 7
println(numero)
numero =99 //ahora la variable número almacena el valor 99
println(numero)
}
Orden de acciones en la sentencia de asignación
Utilizando el operador de asignación “=” es posible que una variable modifique su valor en función de su valor actual
fun main() {
var x=3
var y=7
println(x)
x= 5+1// se calcula 5+1 y se asigna el resultado a x
println(x)
x= y+4 //se lee el valor de y, se le suma 4 y el resultado se asinga a x
println(x)
}
Veamos ahora una sentencia de asignación en la que la misma variable aparece a la izquierda y derecha del igual. Es algo muy habitual y tienes que entenderlo sin titubeos.
fun main() {
var numero=3
numero=numero +1
println(numero)
}
El ejemplo anterior imprime un 4. Para entender sentencias como
numero=numero +1
simplemente tienes que tener claro el funcionamiento general de la sentencia de asignación:
Se evalúa (calcula) la parte derecha
Se asigna dicho valor a la variable de la izquierda
Es muy importante que tengas en cuenta que los pasos de arriba ocurren en secuencia, primero se ejecuta el paso 1 y a continuación el paso 2. De esta manera es sencillo el razonamiento de
numero=numero +1
Al calcular el valor de la derecha ocurre que numero almacena el valor 3. Por tanto se calcula “3+1” que vale 4.
El valor 4 se mete en la variable número y se “machaca” o sobreescribe su viejo valor con lo que ahora numero almacena el valor 4.
Literales
los valores constantes que aparecen en el código se llaman literales o equivalentemente constantes literales. En el siguiente ejemplo
var nombre = "Juan Perro"
var edad = 15
“Juan Perro” y 15 son literales.
Expresiones
De forma informal, una expresión es una especie de frase o fórmula formada por una combinación de variables, literales, operadores y otros recursos que finalmente se pueden evaluar, es decir, calcular para reducir la expresión a un único valor. Por ejemplo
3+2 es una expresión que al evaluarse se reduce al valor 5
“Hola “+ “Winchi” se reduce a “Hola Winchi”
Si a es una variable que almacena el valor 10, la expresión a+3 se evalua y se obtiene el valor 13
etc.
Todos los lenguajes tienen normas muy parecidas para la construcción de expresiones pero cada uno tiene sus pequeños matices. Por ejemplo:
“hola”*2 es una expresión errónea (imposible) en Kotlin, peo es correcta en python que la reduce al valor “holahola”
Uso de variables con la función print()
Ya la utilizamos en ejemplos previos. Simplemente al indicar el nombre de la variable se lee su valor y se imprime
fun main() {
var nombre = "Juan Perro"
var edad = 15
println(nombre)
println(edad)
}
Uso de + y $ con la función print()
El operador + se puede usar para concatenar cadenas de caracteres. Estudiaremos la concatenación de cadenas de caracteres más adelante. Por el momento observamos su uso con print()
fun main() {
var nombre = "Juan Perro"
var edad = 15
println(nombre)
println("Tu nombre: "+ nombre)
println("Tu edad: "+ edad)
}
Observa que para concatenar cadenas de texto literales con variables dentro de un print() necesitamos indicar dicha concatenación con el operador +. Puedes comprobar el error de compilación sin en el ejemplo anterior suprimimos el + en el primer println()
println("Tu nombre: " nombre)
Una forma alternativa de mezclar valores de variables con cadenas de texto es usando el operador $. En este caso escribimos la variable dentro de las comillas de la cadena, no usamos el + y anteponemos el $ al nombre de la variable
fun main() {
var nombre = "Juan Perro"
var edad = 15
println(nombre)
println("Tu nombre: $nombre")
println("Tu edad: $edad")
}
Usar var para crear variables mutables
Mutable significa que a la variable se puede reasignar a un valor diferente después de la asignación inicial. Normalmente esto es lo que deseamos hacer con una variable, ir variando su valor a lo largo del programa.
Para declarar una variable mutable, usamos la palabra clave var como ya hicimos en los ejemplos anteriores. En el siguiente ejemplo observamos que efectivamente una variable declarada con var puede cambiar de valor
fun main() {
var nombre = "Juan Perro"
println("Tu nombre: $nombre")
nombre="otro nombre"
println("Nuevo nombre: $nombre")
}
Usar val para crear variables inmutables
usando val, en lugar de var, una vez que se asigna un valor a la variable, no se le puede cambiar de valor en el resto del programa. Este código genera error
fun main() {
val nombre = "Juan Perro"
println("Tu nombre: $nombre")
nombre="otro nombre"
println("Nuevo nombre: $nombre")
}
A las variables inmutables también se les llama variables de sólo lectura, variables constantes o simplemente constantes
Cuando sabemos que vamos a usar un valor que no tiene ni debe cambiar a lo largo del programa es una buena práctica de programación asignarlo a una variable definida con val
Reglas de nomenclatura de variables de Kotlin
Hay ciertas reglas que se deben seguir al nombrar las variables de Kotlin:
Los nombres de variables de Kotlin pueden contener letras, dígitos, guiones bajos y signos de dólar.
Los nombres de las variables de Kotlin deben comenzar con una letra, $ o guiones bajos
Las variables de Kotlin distinguen entre mayúsculas y minúsculas, lo que significa que Zara y ZARA son dos variables diferentes.
La variable Kotlin no puede tener ningún espacio en blanco u otros caracteres de control.
La variable de Kotlin no puede tener nombres como var, val, String, Int porque son palabras clave reservadas en Kotlin.
Palabras reservadas
Las palabras reservadas, también llamadas clave o predefinidas, son palabras que tienen significados especiales para el compilador y no se pueden usar.
Ya usamos palabras reservadas, por ejemplo:
fun para indicar que vamos a definir una función
var o val para definir una variable
En kotlin se distingue entre palabras reservadas duras y suaves:
duras son aquellas palabras que jamás se pueden usar como identficadores
blandas son aquellas palabras que en ciertos contextos son palabras reservadas pero en otros contextos se pueden usar libremente como identificadores.
Las palabras reservadas se van aprendiendo poco a poco pero veamos una lista de algunas palabras reservadas duras:
var
val
fun
if
else
when
do
for
while
return
class
true
false
etc.
Observa como en el siguiente ejemplo se generan errores de compilación al intentar crear una variable que se llame fun. Recuerda que fun es una palabra reservada y no se puede usar como nombre de variable.
fun main() {
val fun = "Juan Perro"
}
tipos de datos y operadores
Tipos de datos y operadores más básicos
Subsecciones de tipos de datos y operadores
Tipos de datos básicos
Kotlin es un lenguaje de tipado estático, lo que significa que el tipo de datos de cada expresión debe conocerse en el momento de la compilación.
Los tipos básicos
En Kotlin todo es un objeto(entenderemos esto más adelante) y todo objeto tiene un tipo. Los siguientes por su importancia y uso frecuente les llamamos tipos básicos y son:
númericos
Numeros enteros
Byte
Short
Int
Long
Números reales
Float
Double
Boolean
Char
String
Array
Indicar el tipo de una variable
Se puede indicar el tipo de una variable de dos formas:
explicitamente
usando el mecanismo de inferencia automática de tipos
Explicitamente
Indicándolo despues del nombre de la variable y “:”
var unaVariable: Int = 3
val otraVariable: String ="hola"
Con inferencia de tipos
Si no se indica el tipo, Kotlin lo infiere del valor de la expresión a la derecha de la expresión de asignación
var unaVariable = 3 //implica tipo Iint
val otraVariable ="hola" //implica tipo String
Cambio de tipo de una variable
Esta es una cuestión con muchos matices que se abordarán más adelante, pero como punto de partida, una vez que una variable se crea, es de un tipo y mantiene su tipo hasta el final de su vida y no se le pueden asignar valores de diferentes tipos.
var unEntero = 3
unEntero=7 //OK
unEntero="hola" //ERROR
Tipos Numéricos
Tipos para números enteros
Para números enteros, hay cuatro tipos con diferentes tamaños y, por lo tanto, rangos de valores.
Tipo
Tamaño (Bits)
Valor mínimo
Valor máximo
Byte
8
-128
127
Short
16
-32768
32767
Int
32
-2,147,483,648 (-2 31 )
2.147.483.647 (231 - 1)
Long
64
-9,223,372,036,854,775,808 (-2 63 )
9.223.372.036.854.775.807 (2 63 - 1)
Literales para números enteros
Son los literales que especifican un valor numérico. Pueden ser valores enteros o reales.
Los literales enteros se pueden escribir en base 10, base 2 o base 16. No se admite base 8. Para escribir en hexadecimal añadimos como prefijo al literal 0x y en base 2 añadimos 0b
fun main() {
var valor15EnBase10 = 15
var valor15EnBase16= 0xF
var valor15EnBase2= 0b1111
println(valor15enBase10)
println(valor15enBase10)
println(valor15EnBase2)
}
Por otro lado los literales pueden ser Int y Long. Para almacenarlos con 64 bits específicamente hay que añadir una L al final del número, por ejemplo
si usamos 1L, el valor 1 en este caso se almacena en 64 bits
Inferencia de tipos enteros
Todas las variables inicializadas con valores enteros que no excedan el valor máximo de Int tienen el tipo inferido Int. Si el valor inicial excede este valor, entonces el tipo es Long. Para especificar que el valor sea explícitamente Long se agrega el sufijo L al valor.
val one = 1 // Int
val threeBillion = 3000000000 // Long
val oneLong = 1L // Long
val oneByte: Byte = 1 //Byte
Tipos para números reales
Tipo
Tamaño (Bits)
Valor mínimo
Valor máximo
Float
16
1.40129846432481707e-45
3.40282346638528860e+38
Double
32
4.94065645841246544e-324
1.79769313486231570e+308
Literales reales
Para especificar explícitamente el tipo Float de un valor se usa el sufijo f o F. Si dicho valor contiene más de 6-7 dígitos decimales, se redondeará.
val pi = 3.14 // Double
// val one: Double = 1 // Error: type mismatch
val oneDouble = 1.0 // Double
val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float, actual value is 2.7182817
Tipos básicos No Numéricos
Breve introducción. Se veran con más detalle más adelante
Boolean
El tipo Boolean representa objetos booleanos que pueden tener dos valores: true y false.
fun main() {
val a: Boolean = true
val b: Boolean = false
println("Value of variable a "+ a )
println("Value of variable b "+ b )
}
Char
Se usa para almacenar un solo carácter. Un valor Char debe estar entre comillas simples, como ‘A’ o ‘1’.
fun main() {
val letter: Char
letter = 'A'
println("$letter")
}
Secuencias de escape para representar caracteres
Una secuencia de escape esta formada por una barra inversa seguida de una letra, un carácter o de una combinación de dígitos. Una secuencia de escape siempre representa un solo carácter aunque se escriba con dos o más caracteres.
Por ejemplo:
\t para tabulador
\n para salto de línea
\"" para comillas dobles
otros
Las secuencias de escape se usan en varias situaciones. Iran salidendo casos a lo largo del curso en el que necesitaremos el uso de secuencias de escape y los iremos explicando a medida que surjan. Veamos un ejemplo sencillo, queremos insertar dentro de un String un salto de línea
fun main() {
println("A continuación viene un salto de línea\n y ya estoy en otra línea")
}
Otros
Otros tipos muy importantes, que se utilizan mucho ya desde los primeros pasos en programación como String y Array se explicarán en lecciones a parte.
Conversión de Tipos
La conversión de tipos es un proceso en el que el valor de un tipo de datos se convierte en otro tipo. Kotlin no admite la conversión directa entre tipos . Por ejemplo, no es posible convertir un tipo Int a un tipo Long. El siguiente código genera error de compilación
fun main(args: Array<String>) {
val x: Int = 100
val y: Long = x // ERROR
println(y)
}
Para convertir un tipo de datos a otro tipo, Kotlin proporciona un conjunto de funciones:
toByte()
toShort()
toInt()
toLong()
toFloat()
toDouble()
toChar()
toString()
ReescribImos el ejemplo anterior para que sea OK
fun main(args: Array<String>) {
val x: Int = 100
val y: Long = x.toLong()
println(y)
}
Observa que las funciones de conversión se escriben
nombreVar.funConversion()
Realmente es una forma especial de función llamada método, concepto que estudiaremos y entenderemos más adelante.
Conversiones y el operador +
el operador + con números hace sumas aritméticas pero con strings concatena las cadenas. ¿Que ocurre si + tiene un operador de tipo String y otro de tipo numérico?
En el siguiente ejemplo se observa que el + convierte automáticamente el número a String y finalmente concatena ambos operadores
fun main() {
val saludo="hola"
val numero=7
println(saludo + numero)
}
En cambio, si el primer operando es el de tipo numérico da error de compilación
fun main() {
val saludo="hola"
val numero=7
println(numero + saludo)
}
la solución para este tipo de situación es usar toString() de forma que el dato numérico lo convierte a String y el + simplemente concatena Strings
fun main() {
val saludo="hola"
val numero=7
println(numero.toString() + saludo)
}
Operadores
Un operador es un símbolo que le dice al compilador que realice manipulaciones matemáticas o lógicas específicas. Kotlin es rico en operadores integrados y proporciona los siguientes tipos de operadores:
Operadores aritméticos
Operadores relacionales
Operadores de Asignación
Operadores unarios
Operadores logicos
Operacdores de nivel de bit
Operadores aritméticos
Se utilizan para realizar operaciones matemáticas básicas: suma, resta, multiplicación, y división.
Operador
Nombre
descripción
Ejemplo
+
Suma
Suma dos valores
x+y
+
Suma
Suma dos valores
x+y
-
Resta
Resta un valor de otro
x-y
*
Multiplicación
Multioplica dos valores
x*y
/
División
Obtiene el cociente de dividir un valor por otro
x/y
%
Módulo
Obtiene el resto de dividir un valor por otro
x%y
fun main() {
val x: Int = 40
val y: Int = 20
println("x + y = " + (x + y))
println("x - y = " + (x - y))
println("x / y = " + (x / y))
println("x * y = " + (x * y))
println("x % y = " + (x % y))
}
Operadores relacionales
Los operadores relacionales (de comparación) se utilizan para comparar dos valores y devuelven un valor booleano : true o false .
Operador
Nombre
Ejemplo
>
mayor que
x>y
<
mayor que
x>y
>=
mayor o igual que
x>=y
<=
menor o igual que
x<=y
==
igual a
x==y
!=
distinto que
x!=y
fun main() {
val x: Int = 40
val y: Int = 20
println("x > y = " + (x > y))
println("x < y = " + (x < y))
println("x >= y = " + (x >= y))
println("x <= y = " + (x <= y))
println("x == y = " + (x == y))
println("x != y = " + (x != y))
}
Operadores de asignación de Kotlin
Como ya sabemos, el operadores de asignación = se utilizan para asignar valores a las variables. Este es el operador de asignación más importante y habitual pero hay otros complementarios.
Operador
Ejemplo
Forma expandida
=
x=10
x=10
+=
x+=10
x=x+10
-=
x-=10
x=x-10
*=
x*=10
x=x*10
/=
x/=10
x=x/10
%=
x%=10
x=x%10
fun main() {
var x: Int = 40
x += 5
println("x += 5 = " + x )
x = 40;
x -= 5
println("x -= 5 = " + x)
x = 40
x *= 5
println("x *= 5 = " + x)
x = 40
x /= 5
println("x /= 5 = " + x)
x = 43
x %= 5
println("x %= 5 = " + x)
}
Operadores unarios
Los operadores unarios requieren solo un operando; realizan varias operaciones, como incrementar/disminuir un valor en uno, negar una expresión o invertir el valor de un booleano.
Operador
Nombre
Ejemplo
+
más unario
+x
-
menos unario
-x
++
incrementar en 1
x o x
–
disminuir en 1
–x o x–
!
invierte el valor de un booleano
!x
fun main() {
var x: Int = 40
var b:Boolean = true
println("+x = " + (+x))
println("-x = " + (-x))
println("++x = " + (++x))
println("--x = " + (--x))
println("!b = " + (!b))
}
Los operadores ++ y – tienen forma prefija y forma postfija. Estas formas no son siempre equivalentes , esto se estudiará más adelante, por el momento si se utiliza sobre variables aisladas, no insertas en una expresión más complejas, son equivalentes.
Operadores lógicos
Los operadores lógicos son: && || !
Trabajan con valores booleanos.Los vemos en el documento expresiones booleanas
Precedencia de operadores y uso de paréntesis
Observa la expresión 2+3*4. Siguiendo las reglas de las matemáticas tradicionales sabemos que el * tiene más precedencia que el + y por tanto la expresión anterior es equivalente a 2+(3*4). En los lenguajes de programación el concepto de precedencia de operadores es similar a la precedencia con los operadores de las matemáticas tradicionales, así que, razonando matemáticamente cubrimos la mayor parte de los casos de precedencia en expresiones de programación. Hay no obstantes ciertos matices y diferencias entre ambos . Si dudas, simplemente, ¡usa paréntesis!. Entiende a la perfección la salida del siguiente ejemplo:
fun main() {
val a=2
val b=3
//quiero sumar a y b y el resultado multiplicarlo por 4
val resultado1=(a+b)*4
//las dos siguientes expresiones son equivalentes.
val resultado2=a+b*4
val resultado3=a+(b*4)
println(resultado1)
println(resultado2)
println(resultado3)
}
Mezcla de tipos en expresiones.
Es una cuestión con muchos matices. Por el momento, para simplificar, utiliza operandos del mismo tipo.
si mezclamos tipos pueden ocurrir una diversidad de situaciones, por ejemplo, el siguiente código da como salida false
fun main() {
var x:Int=1
var y:Double=1.0print(x>y)
}
y en cambio este da error de compilación
fun main() {
var x:Int=1
var y:Double=1.0print(x==y)
}
Por el momento, para simplificar, utiliza operandos del mismo tipo. Recuerda que cuando necesites cambiar un valor de un tipo a otro dispones de funciones de conversión de tipo.
Expresiones booleanas
Muchas veces nos encontramos con una situación en la que necesitamos tomar una decisión del tipo Sí o No , o queremos indicar que algo es Verdadero o Falso (true o false). Para manejar tal situación usamos el tipo de datos booleano.
Literales booleanos
Son las palabras reservadas true y false.
Variables booleanas
Una variable booleana se crea con el tipo Boolean y puede almacenar los valores true o false
fun main(args: Array<String>) {
val isSummer: Boolean = true
val isCold: Boolean = false
println(isSummer)
println(isCold)
}
Operadores booleanos o lógicos
Operador
Nombre
Descripción
Ejemplo
&&
Y lógico (and lógico)
Devuelve verdadero si ambos operandos son verdaderos
x&&y
||
O lógico (or lógico)
Devuelve verdadero si alguno de los operandos es verdadero
x||y
!
No lógico (not)
Es unario. Devuelve falso si el operando es verdadero y viceversa
!x
fun main() {
var x: Boolean = true
var y:Boolean = false
println("x && y = " + (x && y))
println("x || y = " + (x || y))
println("!y = " + (!y))
}
Expresiones booleanas con operadores relacionales
Una expresión booleana tras ser calculada da true o false. Las expresiones booleanas pueden formarse con operadores booleanos como vimos en el ejemplo anterior, pero también con expresiones con operadores relacionales
fun main() {
val x: Int = 40
val y: Int = 20
val z:Boolean=false
println(x > y || x==y)
println(x > y && x==y)
println(z || x==y)
}
colecciones básicas
Las colecciones son utilizadas para agrupar y manipular conjuntos de datos de manera eficiente. Proporcionan métodos y operaciones para agregar, eliminar, buscar y acceder a los elementos almacenados en ellas.
Las colecciones más básicas del lenguaje kotlin son: strings, arrays, listas, rangos y mapas.
Por el momento estudiaremos sólo Strings y haremos una breve reseña a las listas y rangos ya que con estos recursos nos basta para enfrentarnos a gran cantidad de interesantes retos algorítimicos sin necesidad de manejar ingentes cantidades de sintaxis. Recuerda que lo más importante en un estudiante de programación es desarrollar la capacidad de enfrentarse a problemas. El conocimiento profundo de un lenguaje es importante pero un escalón inferior a la capacidad mencionada.
Subsecciones de colecciones básicas
String
Los objetos Strings se utilizan constatemente en programación y son importantes e inevitables incluso para hacer programas básicos. Por esta razón vemos ahora recursos esenciales para trabajar con Strings para poder incorporar estos recursos en nuestros mini programas. Por otro lado, a lo largo del curso seguiremos ampliando información sobre este importantísimo objeto.
Qué es un String
Es un objeto que contiene una cadena de caracteres.
Literales String
Hay dos tipos de literal String:
El String escapado: se declara entre comillas dobles (" “) y puede contener caracteres de escape como ‘\n’, ‘\t’, ‘\b’, etc.
El String sin formato(raw String): se declara entre comillas triples (”"" “”") y puede contener varias líneas de texto sin ningún carácter de escape.
fun main() {
val escapedString : String ="I am escaped String!\n"var rawString :String ="""This is going to be a
multi-line string and will
not have any escape sequence""";
print(escapedString)
println(rawString)
}
Concatenación en Strings con el operador +
Ya indicamos con anterioridad que el efecto del operador + con Strings es la concatenación. Se pueden concatenar literales, variables o una mezcla
fun main() {
var palabra1 : String ="Hola"var palabra2 : String ="Mundo"var miString: String
miString=palabra1+palabra2
println(miString)
println("Adios"+" mundo cruel")
}
String Templates (plantilla en String)
Son fragmentos de código que se evalúan y cuyos resultados se insertan en la cadena. Una plantilla comienza con un signo de dólar $ y puede constar simplemente de un nombre de variable o de una expresión más compleja entre {}.
fun main() {
var unString : String ="Hola 2 +2 es ${2+2}" println(unString)
var otroString: String ="Insertamos el contenido de unString $unString" println(otroString)
}
Indices en String
Un String es una secuencia de caracteres y se puede acceder individualmente a cada uno de ellos especificando un índice asociado a la posición del caracter .
Al primer caracter le corresponde el indice 0, al segundo el 1, …
El índice se indica entre corchetes []
fun main() {
var saludo : String ="Hola mundo" println(saludo[0])
println(saludo[2])
println(saludo[4])//el quinto caracter es un espacio println(saludo[7])
}
El String es un objeto
En Kotlin todo es un objeto. Un String también es un objeto. Aun no estudiamos que es un objeto pero para trabajar con Strings no es necesario avanzar que un objeto consta de propiedades y funciones. Para acceder a ambos utilizamos el operador “.”.
Propiedades del objeto String
Cuando estudiemos programación orientada a objetos entenderemos al cien por cien que es una propiedad. Por el momento piensa que una propiedad es uuna suerte de variable interna del objeto a la que se puede acceder a través del operado punto
El objeto String tiene dos propiedades usadas frecuentemente:
length
lastIndex. Su valor es equivalente a length -1 ya que recuerda que los índices comienzan a numerarse por 0
fun main() {
var saludo : String ="Hola mundo" println(saludo.length)
println(saludo.lastIndex)
}
A las funciones también se accede con el operador punto. El resto de este capítulo muestra ejemplos de algunas de las funciones más importantes del objeto String
Los String son inmutables.
Los strings Kotlin son inmutables. Esto significa que una vez que se crea un string, no se puede modificar. Cualquier operación que cambie el contenido de un string crea un nuevo string.
Por ejemplo, el siguiente código crea un nuevo string cada vez que se llama a la función toUpperCase():
fun main() {
val cadena ="Hola, mundo!" println(cadena.uppercase()) // Imprime "HOLA, MUNDO!" println(cadena) // Imprime "Hola, mundo!" y demuestra que el String inicial no se modificó}
Si queremos trabajar con el nuevo String debemos engancharlo a una variable
fun main() {
val cadena ="Hola, mundo!" val nuevoString=cadena.uppercase()
println(nuevoString)
}
Si queremos conseguir el efecto anterior pero sin crear una variable adicional, podemos asignar el nuevo string a la variable inicial.
Observa que ahora como la variable puede cambiar de String tiene que definirse como var.
fun main() {
var cadena ="Hola, mundo!" cadena=cadena.uppercase()
println(cadena)
}
ya que los String son inmutables, operaciones de asignación con el operador [] no están permitidas.
el siguiente código genera error de compilación
fun main() {
var cadena ="Hola, mundo!" cadena[0]='X'//cambiar H por X así no es posible}
Funciones del objeto String
Cuando estudiemos programación orientada a objetos entenderemos al cien por cien que es una función miembro asociada a un objeto y por tanto una función de un String. Por el momento es suficiente pensar que las funciones de un String son funciones cuyos datos de trabajo son los datos del String. Se accede a ellas a través del operador punto. Veremos algunas de las funciones más relevantes de los objetos String.
equals() para comparar dos cadenas
Se puede utilizar para comprobar igualdad el operador == o el método equals().
fun main() {
var str1 ="hola"var str2 ="mundo"var str3 = str1+str2
var str4 = str1+str2
println(str3.equals("holamundo"))
println(str3=="holamundo")
println(str3==str4)
}
uppercase() y lowercase()
uppercase() y lowercase() para convertir una cadena en mayúsculas y minúsculas, respectivamente.
fun main() {
var saludo : String ="Hola Mundo" println(saludo.lowercase())
println(saludo.uppercase())
}
drop() y dropLast() eliminar los primeros o los últimos caracteres de una cadena
fun main() {
var saludo : String ="Hola Mundo" println(saludo.drop(2))
println(saludo.dropLast(2))
}
indexOf() para encontrar posición de una subcadena
fun main() {
var frase : String ="Siempre me dices que la vida que llevo es horrible" println(frase.indexOf("que")) //17 es la posición del primer que println(frase.indexOf("la"))
println(frase.indexOf("jamones"))
}
Ejecuta el código y comprueba los índices que devuelve. Observa que cuando no existe el substring que se le indica devuelve -1
subSequence() para indicar un subtring por índice
fun main() {
val str1 ="abcdefghij" val startIndex = 2
val endIndex = 7
val substring = str1.subSequence(startIndex, endIndex)
println("El substring es : "+ substring)
}
Ejecutando el ejemplo anterior observa que:
el límite inferior indicado es inclusivo
PERO, el límite superior es exclusivo
La función replace()
Tiene varias sintáxis pero lo más básico es indicar el String que quiero cambiar por el nuevo. A menudo el String que se quiere cambiar consiste como en el ejemplo en único caracter pero no necesariamente.
fun main() {
var str ="la vida es dura" val oldValue ="a" val newValue ="i" val output = str.replace(oldValue, newValue)
print(output)
}
Un uso muy habitual de replace() es eliminar espacios en blanco
fun main() {
var str =" la vida es dura " val oldValue =" "//espacio en blanco val newValue =""//String vacío val output = str.replace(oldValue, newValue)
print(output)
}
Funciones de conversión de String a otro tipo de dato
Hay una serie de funciones para convertir un String en otro tipo de dato. Las de uso más inmediato necesarias ya en los primeros pasos de programación, son las funciones que convierten un String en un formato numérico como toInt(), toDouble() etc.
Asegurate de entender a la perfección la salida del siguiente código
fun main() {
val str1 ="1" val str2 ="2" val num1=1
val num2=2
println(str1+str2)
println(num1+num2)
println(str1.toInt()+str2.toInt())
}
Observa que el String que se quiere pasar a formato numérico tiene que contener sólo los caracteres del número, incluso los espacios en blanco al final generan error.
fun main() {
val x ="1" val y ="1 "//hay un espacio al final println("x convertido vale "+ x.toInt())
println("a ver que paso cuando lo intento con y")
println("y convertido vale "+ y.toInt())
}
Listas
Una lista es un conjunto de valores del mismo tipo a los que podemos acceder a través de una sola variable. Por lo tanto la operación más básica con una lista es crearla y asignarla a una variable
val miLista = listOf(1, 2, 3)
¿Como se accede a cada elemento individual de la lista?. En una lista cada elemento tiene una posición y podemos usar dicha posición para acceder a cada elemento individual. El concepto y sintaxis es similar al acceso por índices estudiado con Strings.
fun main() {
//val miLista: List<Int> = listOf(1, 2, 3)//el tipo en este caso lo puede inferir el compilador val miLista = listOf(1, 2, 3)
println("imprimir toda la lista junta $miLista") // [1, 2, 3] println("imprimir la lista elemento a elemento ")
println(miLista[0])
println(miLista[1])
println(miLista[2])
println("el tamaño de la lista es: "+ miLista.size)
}
Hay dos tipos básicos de listas:
inmutables
mutables. Permiten modificar el valor de sus elementos así como añadir/borrar elementos a la lista, es decir, modificar el tamaño de la lista.
El ejemplo anterior de listas se corresponde con una lista inmutable, para lo cual utilizamos la función listOf(). A continuación veremos un ejemplo de lista inmutable.
Ejemplo de lista mutable
Hay varias formas de crear una lista mutable, Vemos un ejemplo con mutableListOf()
fun main() {
val colorsList = mutableListOf("Amarillo", "Azul", "Rojo")
colorsList.add("Verde") // [Amarillo, Azul, Rojo, Verde] //inserta al final colorsList.add(0, "Blanco") // [Blanco, Amarillo, Azul, Rojo, Verde]//inserta en la posición indicada indicada colorsList.removeAt(2) // [Blanco, Amarillo, Rojo, Verde]//observa como modificamos con [] colorsList[1]="Negro"// [Blanco, Negro, Rojo, Verde] println(colorsList)
println(colorsList[0])
}
declarar una lista mutable de tamaño 0 (vacía)
Podemos querer ir construyendo una lista partiendo de una lista vacia. Al partir de una lista vacía Kotlin no puede inferir el tipo de la lista. La solución es incluir el tipo en la declaración de la lista de alguna manera como en el ejemplo.
fun main(){
var lista= mutableListOf<Int>()//lista de Int de tamaño 0 println(" tamaño lista ${lista.size}")
lista.add(99)
println(" tamaño lista ${lista.size}")
}
La funcion split() de los Strings
split() permite trocear o dividir un String en trocitos más pequeños y estos trozos los devuelve en un lista. Como parámetro se le indica el criterio de división o delimitador.
Por ejemplo el delimitador en el siguiente ejemplo es el String “:”
fun main() {
val str ="A:B:C:que bonito:z zz" val delim =":" val list = str.split(delim)
println(list) // [A, B, C, que bonito, z zz]}
El delimitador realmente es una expresión regular pero de momento con pensar que es un delimitador es un caracter que se utiliza como punto de corte es suficiente.
Uno de los usos más frecuentes es querer dividir un texto en palabras utilizando como delimitador el espacio en blanco.
fun main() {
val str ="Había una vez un circo que alegraba siempre la ilusión" val delim =" " val list = str.split(delim)
println(list) // [Había, una, vez, un, circo, que, alegraba, siempre, la, ilusión]}
Utilizaremos split() para com combinar con el readln() para conseguir un estilo de entrada de datos por teclado que veremos más adelante.
Rangos
Un rango en Kotlin es tipo que engloba un conjunto de valores que representa el concepto matemático de intervalo de valores. Es decir, es un subconjunto de elementos comprendidos entre un extremo inferior a y un extremo superior b.
Por ejemplo, el rango [0,5] representa los valores enteros del 0 al 5. Para crearlo se usa la función operador toRange()
fun main() {
val fromZeroToFive = 0.rangeTo(5)
println(fromZeroToFive) // 0..5}
Otra sintaxis alternativa y más usada en la práctica es el formato a..b .
fun main() {
val fromZeroToFive = 0..5println(fromZeroToFive) // 0..5}
La aplicación práctica de los rangos la veremos al estudiar estructuras de control(if,for etc.). Ahora nos centramos brevemente en su concepto.
Rangos con extremos inclusivos y exclusivos
Ya que un rango representa un intervalo matemático se me viene a la cabeza lo de intervalo abierto, cerrado, abierto por la derecha pero cerrado por la izquierda etc. que finalmente consisten en pensar si al especificar los extremos están incluidos o no. Piensa que esto es un problema cotidiano, si te digo, tienes hasta el viernes para entregar el trabajo, tu me preguntarás… ¿con el viernes incluido?
Rangos con extremos inclusivos con ..
Sintácticamente se consiguen como vimos más arriba, con los ..
fun main() {
val rangoInclusivo = 1 .. 5 // El rango incluye 1, 2, 3, 4 y 5. println(rangoInclusivo) // 0..5}
Rango con extremo superior no inclusivo con until
fun main() {
val rangoInclusivo = 1 until 5 // El rango incluye 1, 2, 3, 4 println(rangoInclusivo) // 0..4}
Rango con extremo inferior no inclusivo.
No hay una sintaxis propia. Si el intervalo es de números enteros simplemente se incrementa en 1 el extremo inferior.
val rangoExclusivoPorIzquierda = (inicio + 1)..fin
Especialmente interesante puede ser “imitar” un intervalo abierto por la izquierda, es decir, con el extremo inferior no incluido cuando el intervalo es de double. Para esto, puedes hacerlo ajustando el límite inferior sumando un valor muy pequeño.
fun main() {
val limiteInferior = 1.0 val limiteSuperior = 5.0 val epsilon = 1e-10 // Valor muy pequeño val rangoExclusivoPorIzquierda = (limiteInferior + epsilon)..limiteSuperior println(rangoExclusivoPorIzquierda)
}
Rangos y el operador in
Puedes usar el operador in para verificar si un valor está contenido dentro de un rango. El operador in devuelve true si el valor está dentro del rango y false en caso contrario.
fun main() {
val limiteInferior = 1.0 val limiteSuperior = 5.0 val epsilon = 1e-10 // Valor muy pequeño val rangoExclusivoPorIzquierda = (limiteInferior + epsilon)..limiteSuperior println(3.2 in rangoExclusivoPorIzquierda)
println(1.0 in rangoExclusivoPorIzquierda)
}
Más sobre rangos.
Hay muchas más detalles adicionales relacionadas con rangos, pero se examinan mejor cuando se usan con estructuras de control. En ese momento ampliaremos más sobre sintáxis y conceptos sobre rangos.
Entrada de datos por teclado con readln()
La forma más basica de introducir datos con teclado es utilizando la función readln().
La función readln() lee una línea de la entrada standard y la devuelve al programa sin incluir el enter que marca el fin de línea.
¿Qué es la entrada standard ?
Cuando a una función como readln() no se le especifica el dispositivo del que tiene que leer la información de entrada, utiliza el dispositivo asociado la entrada standard que por defecto es el teclado. Si no especificamos lo contrario la entrada standar es el teclado.
¿Qué es una línea?
Es similar a una línea de un folio, es decir un conjunto de caracteres. A este conjunto de caracteres se le suele llamar por su nombre en ingles String. La diferencia entre una línea de un folio y una línea en datos informáticos es como se marca el fin de la linea. En un folio el fin de la línea viene determinado por el final físico del folio hacia la derecha, en informática el fin se establece grabando un caracter de fin de línea también llamado caracter de salto de línea.
El carácter de salto de línea
Es un caracter del código ascii igual que la letra ‘A’ o el caracter ‘$’. Concretamente dentro del código ASCII tiene el valor decimal 10 o hexadecimal 0A, pero desde el código fuente se suele suele representar más cómodamente como ‘\n’. Desde el teclado físico, este caracter se envia a través de la tecla conocida por enter, intro return o salto de línea.
El proceso de capturar datos por teclado con readln
El usuario teclea un string de caracteres.
Pulsa enter.
El sistema operativo pone a disposición de la función readln el string terminado con el enter
La función readln devuelve al programa el string sin el enter
fun main() {
println("Teclea tu nombre")
val nombre = readln()
print("¡Hola, $nombre!")
print(" bonito nombre")
}
Observa como efectivamente el readln() no devuelve el enter ya que bonito nombre se escribe a continuación del saludo, sin salto de línea.
Un ejemplo de ejecución podría ser
Teclea tu nombre
koki kiko
¡Hola, koki kiko! bonito nombre
Cómo leer un valor numérico por teclado
Es muy importante tener en cuenta que la función readln siempre devuelve un String. Así que en realidad lo que vamos a discutir a continuación tiene que ver con el procesamiento de un string, no con la entrada de teclado.
Es habitual que un string contenga caracteres numéricos y que se quiera operar numéricamente con ellos. Para poder operar númericamente con un String que contiene un número debemos convertir dicho String explícitamente a formato númerico con las funciones ya vistas en conversión de tipos.
En el siguiente ejemplo la variable numero es realmente un String y la ejecución del programa genera error ya que no se permite hacer multiplicaciones aritméticas con un valor String
fun main() {
println("Teclea un número")
val numero = readln()
val doble =numero*2
println("El doble es $doble")
}
Por lo tanto, debemos convertir a numérico el string que contiene la variable numero. Podemos por ejemplo convertirlo a un número entero con toInt().
fun main() {
println("Teclea un número")
val numero = readln()
val doble =numero.toInt()*2
println("El doble es $doble")
}
Cómo leer varios valores de la misma línea separados entre sí por un espacio en blanco
En general, lo importante es tener encuenta que readln() simplemente devuelve un String, a continuación las instrucciones de mi programa deberán procesar dicho String a nuestro gusto para obtener el efecto deseado.
Por ejemplo, supongamos que la entrada por teclado consiste en introducir tres números enteros en una línea. Los números los vamos a escribir separados entre sí por un espacio en blanco. Queremos averiguar la media aritmética de los tres números, por lo tanto necesito sumar y dividir viéndome entonces forzado a obtener de la línea de entrada tres valores numéricos. Una forma facil es utilizando la función split() de los objetos String. La función split() trocea utilizando como separador el caracter indicado y devolviendo los trozos en una lista. En realidad, el parámetro de split es una expresión regular pero por el momento nos basta con pensar que es un caracter en base al cual hacer el troceo. En nuestro ejemplo un espacio en blanco es el caracter de “troceo”.
fun main() {
val linea= readln()
val lista= linea.split(" ")
val a= lista[0].toInt()
val b= lista[1].toInt()
val c= lista[2].toInt()
val suma= a+b+c
val media= suma/3.0println(media)
}
una posible ejecución
2 5 1
2.6666666666666665
control de flujo
El orden de ejecución de las sentencias de un programa es en principio secuencial, es decir, de arriba hacia abajo, comenzando por la primera hasta llegar a la última. Al orden de ejecución se le llama flujo. El flujo secuencial elemental de un programa se puede alterar con las instrucciones de control de flujo que veremos a continuación
Subsecciones de control de flujo
Control de flujo
Un programa es un conjunto de sentencias. El orden básico de ejecución de las sentencias es secuencial de arriba abajo. No obstante, hay sentencias especiales que pueden modificar este orden secuencial. Al orden de ejecución le llamamos flujo de ejecución y a las sentencias que permiten modificar el flujo secuencial de ejecución se le llaman sentencias de control de flujo.
Las sentecias de control de flujo kotlin son:
Condicionales
if
when
bucles
for
while
break/continue
Sentencia condicional IF
if simple
Permite ejecutar o no un bloque de instrucciones en función de una condición
. Sintaxis:
if (condition) {
// bloque de código ejecutado si la condición es cierta}
La condición será una expresión booleana que al evaluarse valdrá por tanto true/false
fun main() {
val age:Int = 10
if (age > 18) {
print("Adult")
}
}
En este caso no se imprimirá nada. Si inicializamos la variable con 19 o un valor mayor sí se imprimiría “Adult”.
Si el bloque de instrucciones consiste en una única instrucción como en el ejemplo de arriba se pueden omitir la llaves de bloque {}
if .. else
Es una extensión del if simple de forma que ahora hay dos bloques de código un bloque A asociado al if y otro B asociado al else. Si la condición es false, se ejecuta el código del else,
La sintaxis es
if (condition) {
// bloque A se ejecuta si condition es true} else {
// bloque B se ejecuta si condition es false}
fun main() {
val age:Int = 10
if (age > 18) {
print("Adult")
} else {
print("Minor")
}
}
La sentencia if.. else es una expresion
Recuerda que una expresión devuelve un valor como un Int, un String etc.
La sentencia if..else también es una expresión porque devuelve un valor que para simplificar podemos decir que es el valor que se obtiene del bloque A si la condición es cierta o del bloque B si es falsa. En el siguiente ejemplo, ya que la condición es false y el código B consiste simplente en un literal string “Minor”, el valor asociado al código B, y por tanto a la expresión if..else será “Minor”
fun main() {
val age:Int = 10
val result =if (age > 18) {
"Adult" } else {
"Minor" }
println(result)
}
Uso opcional de los {}
Es frecuente que el código A y B se compongan de tan sólo una instrucción y entonces podemos evitar los {} y ver escrito el ejemplo anterior de forma más compacta
fun main() {
val age:Int = 10
val result =if (age > 18) "Adult"else"Minor" println(result)
}
Es importante observar que el if simple no se puede usar como expresión, es obligatoria la existencia del else.
Sobre el valor que devuelve un bloque del if
¿Que pasa si el código de los bloques no se componen de una única instrucción y contiene varias expresiones?. En este caso el valor del bloque será el de la última expresión
fun main() {
val age:Int = 10
val result =if (age > 18) {
println("Hola, la condición es true")
var y = 2+3 // esto no es la última expresión del bloque A"Adult" } else {
println("Hola, la condición es false")
var z = 10/5 // esto no es la última expresión del bloque B"Minor" }
print("Valor asociado al if..else: ")
println(result)
}
if anidado
Cuando una expresión está presente dentro del cuerpo de otra expresión, se denomina expresión anidada. Dentro de un if o un else puede haber otro if “anidado”.
val x = 37
val y = 89
val z = 6
val result =if (x > y) {
if (x > z)
x
else z
} else {
if (y > z)
y
else z
}
return result
escalera if
Hay muchas combinaciones para anidar, pero una muy frecuente es anidar dentro un else un if-else y que esto ocurra en multiples niveles de anidamiento. En este caso el código adopta forma de “escalera” y se puede hacer poco legible
fun main() {
val number = 60
val result =if (number < 0) {
"Numero negativo" } else {
if (number < 10){
"Numero con un único digito " } else {
if (number < 100) {
"Número con 2 dígitos" } else {
"número con más de dos digítos" }
}
}
print(result)
}
Cuando dentro de cada else la única instrucción es otra if-else anidada y teniendo en cuenta que por lo tanto podemos evitar los {} correspondientes podemos escribir todo más legible suprimiendo los {} del else y pegando la palabra if al else
fun main() {
val number = 60
val result =if (number < 0) {
"Numero negativo" } elseif (number <10) {
"Numero con un único digit " } elseif (number <100) {
"Número con 2 dígitos" } else {
"número con más de dos digítos" }
print(result)
}
No es más que un reagrupamiento de lineas pero da la sensación que tenemos una nueva sentencia else if más legible que la escalera inicial
El operador in combinado con if
El operador in se usa para verificar la existencia de un valor dentro de una colección como por ejemplo dentro de un rango o un array. A menudo lo veremos formar parte de la condición de un if com en el ejemplo
fun main() {
val nombres= arrayOf("chosky","chuly", "uinchi")
if ("Pepe" in nombres)
println("Pepe está en el array")
else println("Pepe no está en el array")
}
fun main() {
val edadesPermitidas=10..20 val miedad=15
if ( miedad in edadesPermitidas)
println("Puedes pasar")
}
Sentencia when
Al igual que if es una sentencia y también una expresión y por lo tanto devuelve un valor al ser ejecutada.
Todo lo que se escribe con when se puede escribir con if else pero hay en situaciones en las que when genera un código más limpio y fácil de leer y se prefiere a su equivalente if else.
Fíjate en el siguiente ejemplo para observar la estructura de esta sentencia. Entre los paréntesis del when va una expresión que finalmente al evaluarla se reducecude a un valor, en el ejemplo, la expresión es una simple variable y su valor es 2. Evaluada la expresión del when, a continuación se examina secuencialmente cada línea que llamaremos en este caso rama. Se avanza pues secuencialmente de rama en rama hasta que se encuentra una cuyo valor coincide con el valor de la expresión anterior. Si se encuentra coincidencia se ejecutan ejecutan exclusivamente las acciones de dicha rama y con esto finaliza la ejecución del when ignorándose el resto de las ramas aun no examinadas. Es típico que la última rama sea un else para que funcione como opción por defecto cuando el resto de las ramas no se cumplen
fun main() {
val day = 2
when (day) {
1 -> println("Monday")
2 -> println("Tuesday")
3 -> println("Wednesday")
4 -> println("Thursday")
5 -> println("Friday")
6 -> println("Saturday")
7 -> println("Sunday")
else-> println("Invalid day.")
}
}
a continuación vemos alguna posibilidad sintáctica que se ve muy frecuentemente con when
Agrupar varios valores en una rama
fun main(args: Array<String>) {
val day = 2
when (day) {
1, 2, 3, 4, 5 -> println("Weekday")
else-> println("Weekend")
}
}
Usar rangos en las ramas
fun main() {
val day = 2
when (day) {
in 1..5-> println("Weekday")
else-> println("Weekend")
}
}
Usar expresiones en lugar de valores en la parte izquierda de la rama
Realmente como una expresión una vez evaluada se reduce a un valor, las consideraciones son las mismas que cuando escribimos un valor sencillo.
fun main() {
val x = 20
val y = 10
val z = 10
when (x) {
(y+z) -> print("y + z = x = $x")
else-> print("Condition is not satisfied")
}
}
Una rama puede contener un bloque de código
En este caso, hay que usar llaves para delimitar el bloque
fun main() {
val day = 2
when (day) {
1 -> {
println("First day of the week")
println("Monday")
}
2 -> {
println("Second day of the week")
println("Tuesday")
}
3 -> {
println("Third day of the week")
println("Wednesday")
}
4 -> println("Thursday")
5 -> println("Friday")
6 -> println("Saturday")
7 -> println("Sunday")
else-> println("Invalid day.")
}
}
Como when es una expresión, se puede usar su valor.
La rama elegida tendrá a la derecha de -> un valor, expresión o bloque que devuelve un valor. Es el valor que se asocia en su conjunto a toda la expresión when
fun main() {
val day = 2
val result = when (day) {
1 ->"Monday" 2 ->"Tuesday" 3 ->"Wednesday" 4 ->"Thursday" 5 ->"Friday" 6 ->"Saturday" 7 ->"Sunday"else->"Invalid day." }
println(result)
}
Bucle for
¿Qué son los bucles?
Imagina una situación en la que necesite imprimir en pantalla una oración 20 veces. Podemos escribir println(oracion) 20 veces, pero, ¿Qué pasa si necesitas imprimir la misma oración mil veces? Aquí es donde necesitamos usar bucles para simplificar el trabajo de programación. En realidad, los bucles se utilizan en la programación para repetir un bloque específico de código hasta que se cumpla una determinada condición.
A los bucles también se les llama a menudo por su nombre en inglés loop o el nombre más formal de estructura iterariva.
Kotlin admite varios tipos de bucles y en este documento veremos el bucle for.
Sintaxis del bucle for
for (item in colección) {
// cuerpo del bucle
}
Veremos más adelante que es una colección, por el momento, informalmente, digamos que es una colección de elementos como los rangos, arrays o Strings.
El bucle ejecuta el cuerpo tantas veces como elementos tenga la colección, es decir, itera sobre la colección y en cada iteracción o vuelta devuelve un elemento de la colección en la variable . Observa que in es un operador que ya vimos al estudiar rangos y que es un elemento obligatorio en la sintaxis del for.
Iterar sobre un String
En el siguiente ejemplo, en cada iteración se imprime una letra de la palabra “hola”
fun main() {
for (item in "hola") {
println(item)
}
}
Iterar sobre un rango
fun main() {
for (item in 1..5) {
println(item)
}
}
El desplazamiento a través del rango es de uno en uno, es decir, en cada paso incremento el desplazamiento dentro del rango en 1. Con la palabra reservada step puedo indicar otro incremento de desplazamiento.
fun main() {
for (item in 1..10 step 2) {
println(item)
}
}
Puede querer iterar sobre un rango pero comenzado por el último elemento y avanzando descendentemente utilizando la palabra reservada downTo
fun main() {
for (item in 5 downTo 1 step 2) {
println(item)
}
}
Bucles anidados
Estudiamos anteriormente que algunas sentencias como el if se pueden anidar. También es posible anidar bucles. Veamos un ejemplo con el bucle for
fun main() {
for(i in 1..3)
for(j in 1..3)
println("$i$j")
}
Hay un bucle “exterior” controlado por una variable i y otro “interior o anidado” controlado por j. En cada paso del bucle exterior se ejecuta el bucle interior o lo que es lo mismo en este caso para cada valor de i se ejecuta el bucle interior. Tienes que entender a la perfección la salida del código anterior.
En ocasiones se necesitan niveles extra de anidamiento pero es más infrecuente
fun main() {
for(i in 'a'..'b')
for(j in 1..2)
for(k in 'x'..'z')
println("$i$j$k")
}
while y do while
while
sintaxis
while (condition) {
// body of the loop}
Mientras la condición sea verdadera se ejecuta el cuerpo del bucle
fun main() {
var i = 5
while (i > 0) {
println(i)
i-- }
}
Observa que:
a menudo vamos a precisar declarar una variable externa al bucle para controlar la condición de salida, a dicha variable se le suele llamar con el termino contador
tenemos que escribir dentro del cuerpo del while como queremos que se incremente/decremente el contador.
sería posible que el cuerpo se ejecute 0 veces. Decimos que el cuerpo de un bucle while se puede ejecutar de 0 a n veces
do while
Similar al while con la siguientes diferencias
la condición del bucle se escribe y comprueba despues de ejecutar el cuerpo
por lo tanto, el cuerpo se ejecuta al menos 1 vez. Decimos que el cuerpo se ejecuta de 1 a n veces
sintaxis
do{
// body of the loop}while( condition )
fun main() {
var i = 5;
do{
println(i)
i-- }while(i > 0)
}
Bucles infinitos
Son bucles con infinitas iteraciones.
fun main() {
while(true)
System.out.println("Hasta el fin de los tiempos....")
}
Aunque ahora pueda resultar sorprendente, los bucles infinitos se utilizan provechosamente en diversas situaciones. También hay que tener en cuenta que podemos generar un bucle infinito de forma no intencionada si escribimos mal el código del bucle haciendo por tanto que el programa se “cuelgue” debido a que jamás se llega a cumplir la condición de fin de bucle, normalmente porque el contador que suele ir en la condición del bucle se incrementa incorrectamente o no se incrementa.
fun main() {
var i=0
while(i<6)
System.out.println("i vale $i. i nunca cambia y jamás se llega a cumplir que i>= 6 para que pare el bucle")
}
for vs while
En kotlin se usa más el bucle for por muchas razones:
for suele generar código más conciso y facil de entender, no es raro ver bucles for escritos en una sóla línea.
for es más seguro, por ejemplo, evitan los típicos despistes de incremento/decremento manual de los índices que controlan el bucle
tiene muchas posibilidades asociadas a técnicas de programación funcional.
No obstante, siempre habrá un buen momento para usar el viejo while y su hermano do while. Nos encontraremos con problemas que para resolverlos es más natural expresar la solución pensando en mientras que …. Por ejemplo, crear un bucle infinito con while(true) es sencillo y elegante. Irán saliendo otras situaciones y casos en lo que preferiremos el while. Nosotros como estudiantes de programación debemos entender y manejar tanto el for como el while.
variables locales y bloques
Variables locales y bloques
Llamemos bloque de instrucciones al conjunto de instrucciones definido entre {}. Al utilizar sentencias condicionales y bucles estos tendrán bloques de instrucciones definidos entre {}. En el siguiente ejemplo observamos que el bloque del cuerpo del while está anidado dentro del bloque de la función main() y observamos como desde el bloque interno se puede acceder a las variables del bloque externo pero no al revés.
fun main() {
var x=20
while(x<20){
x++//puedo usar una variable declarada en un bloque más externovar y= 56
}
//println(y) no se puede usar una variable declarada en un bucle interno}
variables locales de bloques anidados con el mismo nombre
Evita usar variables en bloque externo e interno con el mismo nombre. Es posible hacerlo pero genera confusión al leer el código. En el siguiente ejemplo observamos que la variable x del bloque if oculta a la x del bloque externo en el momento de ejecución del bloque if
fun main() {
var x=20
if(x<100){
var x=8
println(x)
}
println(x)
}
repetir nombres de variables en bloques secuenciales.
Algo que si se utiliza mucho es repetir el mismo nombre para las variables contador de los bucles cuando estos son secuenciales (sin anidar). Así evitamos que tener que estar inventando nombres. En el siguiente ejemplo llamamos siempre i a los contadores. Observa que son variables que se crean y destruyen con cada for y no surgen problemas de ambigüedad.
fun main() {
for(i in 1..3) println("Hola mundo")
for(i in 1..3) println("Adios mundo")
}
break y continue
son formas de alterar la iteración normal de un bucle que en principio está exclusivamente dirigida por la condición del bucle.
break permite finalizar la ejecución del bucle
continue permite finalizar la iteración actual del bucle
Tanto break como contine se pueden utilizar con for, while o do while.
sintaxis
El break y el continue suelen ir dentro de un if o estructura condicional. No tendría sentido práctico ejecutar un break/continue sin verificar previamente que se cumple una condición. Vemos la sintaxis sólo con break teniendo en cuenta que con continue la sintaxis es idética.
// Using break in for loop
for (...) {
if(test){
break
}
}
// Using break in while loop
while (condition) {
if(test){
break
}
}
// Using break in do...while loop
do {
if(test){
break
}
}while(condition)
Si la condición test es cierta, se ejecuta el break/continue.
break
Si la condición test es cierta, se ejecuta el break de forma que inmediatamente se para de ejecutar la iteración actual y se sale del bucle. Por lo tanto despues de un break de un bucle la siguiente instrución a ejecutar será la siguiente instrucción al bucle que no pertenece al bucle
fun main() {
var i = 0
while (i < 100) {
println(i)
if( i == 3 ){
break }
i=i+1
}
}
el código anterior imprimirá los números 0, 1,2 y 3
En cambio, el siguiente código
fun main() {
var i = 0
while (i < 100) {
if( i == 3 ){
break }
println(i)
i=i+1
}
}
imprimirá los números 0, 1 y 2
Tienes que entender perfectamente la diferencia de impresión entre estos dos últimos ejemplos
continue
El siguiente programa imprimirá los números 0, 1,2,4 y 5. No imprime el 3.
fun main() {
var i = 0
while (i< 6) {
if( i == 3 ){
i++continue }
println(i)
i++ }
}
¿Qué ocurriría sin no hay un i++ dentro del if? ¡Prúebalo y razónalo!
Break y continue funcionan por supuesto con do while y con for. Por ejemplo, el código equivalente al anterior con for podría ser:
fun main() {
for (i in 0..5){
if(i==3) continue println(i)
}
}
Otras formas de iterar
Hay otras formas de iterar en kotlin que veremos más adelante y que por el momento simplemente nombramos:
recursividad.
diversas técnicas que utilzan conceptos de programación funcional como las funciones repeat() y foreach() entre otros mecanismos.
Funciones
El concepto de función de programación es parejo al de función matemática, pero incorpora diversos matices para adecuar el concepto matemático de función al mundo de la programación.
Ya utilizamos funciones en nuestros ejemplos, por ejemplo la función print() que es una función escrita por los fabricantes de kotlin y que nosotros podemos usar cuantas veces queramos. Además, en nuestros ejemplos no sólo estuvimos usando funciones ya hechas como print(), también escribimos el código de una función, concretamente escribimos una y otra vez el código de la funcion “especial” main() que será una función que llama o invoca el sistema Kotlin para comenzar la ejecución de nuestra aplicación.
Definir y llamar a una función
Una función es un bloque de código que se escribe para realizar una tarea en particular. Se escribe una vez y luego se puede utilizar o llamar las veces que queramos y gracias a esta característica, las funciones son uno de los mecanismos que tienen los lenguajes de programación para evitar duplicar código.
Para poder disfrutar de una función tendremos que hacer dos cosas:
definir la función
llamar a la función.
Definición de una función
Una función se debe definir según esta estructura básica. A esta estructura básica iremos añadiendo excepciones.
Ejemplo
función que recibe un número entero y devuelve dicho número elevado al cuadrado.
fun elevarAlCuadrado(x: Int): Int {
return x * x
}
los nombres de las funciones en kotlin deben seguir las mismas cuestiones de estilo que las variables.
Por lo tanto para definir una función:
Comenzamos con la palabra reservada fun
Nombre de la función − Es el nombre que eliges para la función con el fin de esclarecer su propósito
Lista de parámetros − . Defínelos como nombre:tipo y sepáralos por comas.
Tipo de retorno − Tipo de dato de salida de la función.
Cuerpo de la función – Son todas las sentencias que realizan la tarea para llegar al resultado final de retorno. Usa la expresión return para devolver el valor.
Llamar a una función
Parámetros y argumentos
Para entender bien el mecanismos de llamada a una función es necesario tener clara la relación entre el concepto de parámetro y el concepto de argumento, que por cierto, es el mismo que en las matemáticas tradicionales:
parámetro. Un parámetro no es más que una variable definida entre los paréntesis de la función.
argumento. Un argumento es un valor que se le pasa a una variable parámetro a través del mecanismo de llamada a una función.
En muchos sitios web usan indistintamente estos dos términos y, además de que no es correcto, puede generarte confusión y falta de claridad en las explicaciones.
El mecanismo de llamada a una función
Una vez que tenemos definida una función, podemos llamar o invocar a una función.
LLamar a una función consiste simplemente en escribir su nombre junto a los valores que queremos que tomen sus parámetros. Recuerda que a los valores que le pasamos a la función le llamamos argumentos. En el siguiente ejemplo, definimos la función, y luego en main() pasamos a usarla/llamarla/invocarla las veces que queramos, en este caso sólo dos veces pero no hay límite de cantidad de llamadas.
Con un poco más de detalle, lo que ocurre cuando se llama a una función es:
los argumentos se asignan a los parámetros
se ejecutan las instrucciones de su cuerpo que probablemente utilicen los valores recibidos por los parámetros
y finalmente como resultado de su ejecución devolverán casi siempre un valor de retorno.
Así por ejemplo, cuando realizamos la primera llamada square(2) ocurre:
Al parámetro x se le asigna el valor “2”. Puedes imaginarte que internamente en la función al ser invocada de esa manera ocurre una instrucción de asignación tipo x=2
se hace el cálculo x*x que es 4
como esta expresión está despues de la palabra reservada return, el valor 4 es lo que finalmente “devuelve” la función.
Funciones con Cuerpo De Expresión
También se les conoce por funciones de una sóla línea.
Si el cuerpo de una función es tan sencillo que consiste en devolver simplemente el valor de una expresión podemos escribir la función con una sintaxis más breve. Volvemos a escribir la función square() con esta sintaxis y observamos:
se escribe en una única línea
desaparecen {} y return.
aparece =
fun square(x: Int): Int = x * x
fun main() {
println(square(2))
println(square(5))
}
Retorno tipo Unit
Toda función tiene que tener un tipo de retorno. Si una función no devuelve ningún valor su tipo de retorno es Unit que es un tipo de retorno especial que justamente indica que la función no devuelve nada.
fun saludar(nombre: String): Unit {
println("Hola, "+ nombre)
}
fun main() {
saludar("Winchi")
}
El tipo de retorno Unit se puede omitir
si en la definición de la función omitimos el tipo de retorno se asume que su tipo es unit.
¿Cuál es el tipo de la famosa función main()? Observarás que no se especifica, por tanto su tipo de retorno es Unit. En el siguiente ejemplo no se especifica el tipo para saludar() y por tanto sabemos que su tipo es Unit, es decir, que no devuelve ningún valor
fun saludar(nombre: String){
println("Hola, "+ nombre)
}
fun main() {
saludar("Winchi")
}
Unit y return
Cuando una función es de tipo Unit, es decir, no devuelve ningún valor , podemos omitir el return cuando dicho return es la última instrucción de la función. En el siguiente ejemplo añadimos el return a saludar() y vemos que el efecto es el mismo que no ponerlo.
fun saludar(nombre: String): Unit {
println("Hola, "+ nombre)
return}
fun main() {
saludar("Winchi")
}
Pero veremos más adelante, que una función no tiene porque tener un único return colocado como última instrucción, así que, lo indicado anteriormente tendrá matices que veremos en su momento.
Named arguments (argumentos con nombre)
Si al llamar a una función, indicamos el nombre del parámetro, podemos cambiar el orden de los argumentos.
fun imprimirXYZ(x: Int, y: Int, z: Int) {
println("x: $x, y: $y, z: $z")
}
fun main() {
imprimirXYZ(1, 2, 3)
//usar named arguments cambiando orden imprimirXYZ(z = 3, x = 1, y = 2)
}
Parametros con valores por defecto
Al definir la función es posible indicar los valores por defecto de los parámetros. Por el momento para simplificar, nos fijamos en el funcionamiento de este mecanismo cuando la función consta de sólo un parámetro.
fun saludar(nombre: String="Churry"){
println("Hola, "+ nombre)
}
fun main() {
saludar("Winchi")
saludar()
}
Se debe procurar poner los parámetros con valor por defecto al final (a la derecha). Los valores que se proporcionan en la llamada se empiezan asignar por la izquierda.
fun imprimirXYZ(x: Int, y:Int=2, z:Int=3) {
println("x: $x, y: $y, z: $z")
}
fun main() {
imprimirXYZ(7,8,9)
imprimirXYZ(7,8)
imprimirXYZ(7)
}
si nos empeñamos en poner primero (a la izquierda) los parámetros con valor por defecto, tenemos que usar argumentos con nombre para llamar la función. En el siguiente ejemplo las llamadas comentadas producen un error de compilación ya que z se queda sin valor.
fun imprimirXYZ(x: Int=1, y:Int=2, z:Int) {
println("x: $x, y: $y, z: $z")
}
fun main() {
imprimirXYZ(7,8,9)
//imprimirXYZ(7,8)// x vale 7, y vale 8, pero z se queda sin valor//imprimirXYZ(7)// x vale 7, y vale 2, pero z se queda sin valor imprimirXYZ(z=9)// x vale 1, y vale 2, y z vale 9}
Variables locales y globales.
variable local: variable que se define dentro de una función
variable global: variable que se declara al principio del programa (al principio del fichero) fuera de toda función.
En el siguiente ejemplo x es una variable global al programa de forma que es accesible desde todas las funciones del fichero, en este caso fn() y main().
var x = 100 // variable globalfun fn() {
x = x + 100
}
fun main() {
println("x vale : $x")
fn()
println("x vale: $x")
}
A lo largo del curso, iremos entendiendo que, salvo en casos especiales, debe evitarse el uso de variables globales ya que generan código inseguro de mala calidad.
variables de ámbito local a una función.
Las variables que se declaran en una función y su funcionamiento es local a esa función, son:
los parámetros, que no son más que un tipo un poco especial de variables locales que toman su primer valor cuando se invoca a la función con argumentos. Aunque ciertamente los parámetros tienen un un ambito local a la función, no uses el término variable local para los parámetros, usa mejor siempre el término parámetro.
las variables locales. Son las variables que se declaran en el cuerpo de la función.
Una variable local sólo es accesible desde la función que la declara. En el siguiente ejemplo observa que la variable x sólo es accesible desde fn() y la variable y desde main(). Si descomentas las instrucciones comentadas observarás el error.
fun fn() {
var x = 100
println(x)
//println(y) //error, y no es una variable conocidad dentro de fn}
fun main() {
var y =10
println(y)
//println(x) //error, x no es una variable conocida dentro de main}
Esta idea es extensible a los parámtros con la salvedad de la asignación entre parámetro y argumento. Observa el error si descomentamos en main() la instrucción comentada
fun fn(x:Int) {
println(x)
//println(y) //error y no es una variable conocidad dentro de fn}
fun main() {
var y =10
fn(7)
println(y)
//println(x) //error, x no es una variable conocida dentro de main}
Por lo tanto, funciones diferentes pueden tener variables con nombre coincidentes, no hay lugar a la confusión ya que realmente son variables diferentes sólo accesibles desde su función.
fun fn1(x:Int) {
println(x)
}
fun fn2(x:Int) {
println(x)
}
fun fn3() {
var x=3
println(x)
}
fun main() {
fn1(1)
fn1(2)
fn1(3)
var x="x de main" println(x)
}
la variable local oculta al parámetro
No tiene sentido práctico inmediato tener en la misma función un nombre de parámetro coincidente con un nombre de variable local, pero es posible.
En caso de coincidencia de nombre lo que ocurre es que ambas variables existen, pero la variable local oculta al parámetro en el sentido que las instrucciones de la función usan la variable local, no el parámetro.
Observa lo comentado en el siguiente ejemplo y simplemente concluye que aunque no genera error, es mejor no usar el mismo nombre para parámetro y variable local para evitar código confuso dificil de entender.
fun f(x:Int):Int{
var x=3
return x
}
fun main() {
println(f(8))
}
funciones con parámetros tipo lista
Cuando se quiere especificar un parámetro de tipo array lo que simplemente queremos es declarar su tipo y lo hacemos indicando
List<tipo>
fun leerLista(miLista: List<Int>) {
for (num in miLista) print("$num ")
}
fun main() {
val unaLista = listOf(1, 2, 3)
leerLista(unaLista)
}
Cuando se pasa una lista como argumento lo que se está pasando es la dirección del array en memoria, no una copia de sus datos, por lo tanto, desde la función el array original es modificable
fun cambiarLista(miLista: MutableList<Int>) {
for (i in miLista.indices) miLista[i]= 999999
}
fun main() {
val unaLista = mutableListOf(1, 2, 3)
cambiarLista(unaLista)
for (num in unaLista) print("$num ")
}
Cuando estudiemos las listas con un poco más de profundidad que lo visto hasta ahora retomaremos esta cuestión.
Sobrecarga de funciones
Sobrecargar una función consiste en definir una función varias veces con el mismo nombre pero con distinto tipo de argumentos y/o número de argumentos. La sobrecarga permite por tanto que una función se comporte de forma diferente en función de la cantidad y tipo de sus parámetros
fun saludar(nombre: String): Unit{
println("Hola $nombre")
}
fun saludar(nombre: String, edad:Int): Unit{
println("Hola $nombre no sabía que tenías $edad años")
}
fun saludar(nombre: String, sueldo:Double): Unit{
println("Hola $nombre no sabía que ganabas $sueldo €")
}
fun main(){
saludar("Chuly")
saludar("Chuly",35)
saludar("Chuly",350.0)
}
funciones built-in o integradas.
El término built-in puede tener significados un poco diferentes según el contexto. Para nosotros ahora, una función built-in, integrada, incorporada o standard entre otros nombres se refiere a las funciones que vienen integradas dentro del propio sdk de kotlin y por tanto podemos usarlas de forma inmediata en nuestro código.
Ya que Kotlin es un lenguaje de POO la mayoría de las funciones se pueden usar asociadas a objetos, recuerda las funciones de los objetos String.
fun main() {
println("Hello World!".reversed())
println("Hello World!".uppercase())
}
Aunque muchas menos que las anteriores, también hay muchas funciones que pueden usarse sin estar asociadas a ningún objeto o variable referencia. Estas funciones están escritas en un paquete. El concepto de paquete lo veremos más adelante pero por el momento lo podemos asimilar al nombre de una carpeta que contiene el fichero que a su vez contiene la función que me interesa. Un paquete con funciones muy usado es el que contiene funciones matemáticas como abs(), max() etc. Observa en el siguiente ejemplo que precisamos una sentencia import para indicar que función de que paquete es la que queremos usar
import kotlin.math.abs
fun main() {
//abs() ya está escrita, simplemente la usamos cuando queramos en nuestro código//imprimir el valor absoluto de -5 println(abs(-5))
}
por comodidad podemos importar todas las funciones de un paquete usando el wildcard *
por otro lado, kotlin es compatible con Java lo que quiere decir entre otras cosas que puede usar su libreria de clases. Podemos importar funciones(realmente métodos) de dichas clases. Observa como podemos obtener de la libreria java la función now() para obtener el instante actual
import java.time.Instant.now
fun main() {
println("instante actual: ${now()}")
}
El paquete standard. ¿Y porque no usamos import para println() o readln()?
El conjunto de funciones y clases proporcionadas por defecto en Kotlin se conoce como el “paquete estándar de Kotlin”. Este paquete contiene un conjunto de funciones y clases que son esenciales para la programación en Kotlin y están disponibles por defecto en cada archivo de código Kotlin sin necesidad de importar nada adicional.
El paquete standard de kotlin se llama simplemente ‘kotlin’ y por tanto con ‘kotlin.*’ importaría todo su contenido
import kotlin.* // import everything from the kotlin packagefun main() {
println("Hello, world!")
}
Pero ten encuenta que la sentencia import anterior es innecesaria pues ya la hace automáticamente kotlin por nosotros.
¿Porqué usar funciones?
La incorporación de las funciones en la programación fue un gran avance, algunas razones:
Se pueden ejecutar más de una vez en un programa y/o en diferentes programas ahorrando tiempo de programación.
Es una forma de compartir código entre programadores.
Es una forma de dividir un problema complejo en problemas simples. cada problema simple sería una función. Esto además facilita la división de tareas entre un equipo de programadores.
Mejora la estructura y legibilidad de un programa.
Se pueden probar individualmente y por tanto facilita el mantenimiento del programa.
Son la base del paradigma de programación funcional que veremos más adelante en el curso.
El valor nulo
En estos momentos, el concepto de nulo,null en inglés, puede resultarte un poco desconcertante pero cobrará sentido poco a poco con la práctica .
El valor null
Es un valor especial que se utiliza para indicar justamente que no hay valor. Por ejemplo, si queremos que una variable en un momento dado no almacene ningún valor, le damos el valor null que a pesar de ser un valor tiene el significado especial de “no valor”.
No pienses que el valor Null es similar a 0(cero) o blanco o una “cadena vacía”, es un valor muy especial y no se comporta como ninguno de esos valores.
El valor null se usaba mucho en los antecesores de Kotlin como C++ y Java. En Kotlin se intenta limitar este uso y esto conlleva tener claro una serie de cuestiones que abordaremos a continuación.
Kotlin es null safety
Kotlin es Null Safety, es decir, que gestiona los nulos de forma segura, de modo que puedes garantizar facilmente que tu código no va a producir NullPointerException (NPE).
¿Qué es NullPointerException?
Por el momento simplemente indicar que es un error que se puede generar al ejecutar un programa que trabaja con nulos.
Intenta intuir que es NullPointerException con el siguiente ejemplo. La segunda instrucción por razones que entenderás más adelante genera NullPointerException y ahí se para en seco la ejecución del programa, ninguna de las instrucciones que siguen a la segunda instrucción se van a ejecutar jamas. ¡Pruébalo!
fun main() {
val x: Int?=null val y = x!!.toDouble()
print("la excepción aborta el programa y nunca se imrprime esto")
println("ni esto ....")
}
¿Son malas las NPE?
No necesariamente. Depende del contexto y estilo de progrmación. Cuando estudiemos el control de excepciones por un lado y porgramación funcional por otro, entenderemos mejor este problema. Por el momento simplemente indicar que al usar en Kotlin técnicas de programación funcional las NPE se convierten en un engorro y se tuvo esto en cuenta en el diseño de Kotlin.
Por defecto una variable no puede tomar el valor null
Esto por ejemplo no compila
val x: Int = null
Marcar el tipo con ? para permitir nulos
Si quieres que una variable acepte nulos, tienes que marcar el tipo con una ?
val x: Int? = null
Añadir un ? al tipo se le denomina definir un tipo como anulable y permite por tanto que el tipo admita en su rango de valores el valor null que por defecto ningún tipo admite.
Chequeo de nulos en tiempo de compilación
Si permitimos expresamente que una variable tome el valor null, el compilador nos puede obligar a comprobar el nulo antes de hacer algo con esa variable para asegurarse de que no se producirá un NullPointerException.
En el siguiente ejemplo simplemente el valor de x lo intento imprimir y no hay problemas de compilación
fun main() {
val x: Int?=nullprint(x)
}
Pero, si quiero cambiar el valor de x de Int a Double invocando la función toDouble() se genera error de compilación
fun main() {
val x: Int?=null val y = x.toDouble()
}
Si kotlin permitiera la ejecución del programa anterior y si además ocurriera que el valor de x fuera null, el programa generaría una excepción NullPointerException. Ya indicamos anteriormente que esto se quiere evitar en Kotlin y por lo tanto en el caso anterior Kotlin ya no deja ejecutar el programa generando un error en la fase previa de compilación.
El operador !!
El método menos seguro para el tratamiento de nulos es simplemente indicar al compilador que evite chequeos en tiempo de compilación respecto a la posibilidad de que se produzca una nullpointerException.
Esta opción tiene sentido si:
estoy completamente seguro que mi variable nunca va a llegarle un valor null
Me da igual si se produce una NullPointerException.
Para indicar al compilador que evite el chequeo usamos el operador !! llamado operador de aserción de no nulo, o sea, que aseguramos al compilador que no se va a producir una NullPointerException y que si se produce asumimos la responsabilidad.
En el siguiente ejemplo al escrib ir x!! no se genera error de compilación pues inhibimos el chequeo de nulos. Pero, ya que x vale null se genera en tiempo de ejecución una NullPointerException
fun main() {
val x: Int?=null val y = x!!.toDouble()
}
En los siguientes apartados veremos otros enfoques de tratamiento nulos y llegaremos a la conclusión que el operador !!" es la opción menos segura, ya que lo único que hacemos es deshabilitar la inspección de nulos del compilador, no obstante, por su sencillez, es el enfoque que usaremos en nuestros primeros programas.
Observa en el siguiente ejemplo como efectivamente usando !! puedo ocurrir un nullPointerException indeseado que para la ejecución del programa. Recuerda que si usas !! la responsabilidad del control de nulos pasa a ser total para el programador que es el que tendrá que asegurarse de que un NullPointerException no va a parar su programa
fun main() {
val x: Int?=null val y = x!!.toDouble()
print("la excepción aborta el programa y nunca se imrprime esto")
println("ni esto ....")
}
A continuación se profundiza un poco más en el tratamiento de nulos pero con lo visto hasta aquí es suficiente por el momento.
if para hacer un tratamiento seguro de nulos
Ahora el compilador no genera error ya que detecta que el código escrito aunque x puede valor null, si esto ocurre, no se ejecuta x.toDouble() ya que lo hemos prevenido con un if y por tanto el código es seguro
fun main() {
val x: Int?=nullif (x!=null){
val y = x.toDouble()
}
}
El if nos ofrece multiples posibilidades, por ejemplo, otra forma típica de gestionar la situación anterior es que si detectamos que x vale null entonces le damos a y un valor que nosotros consideramos apropiado
fun main() {
val x: Int?=null val y =if (x!=null) x.toDouble() else 0.0print(y)
}
Expresión de acceso seguro
Recuerda que Kotlin, por razones que entenderas más adelante, hay cierta obsesión por evitar La nullPointerException, otra forma de tratar de forma de vitarla es usar una expresión de acceso seguro que con este nombre un poco aparatosos consiste simplemente en añadir una ? despues del nombre de la variable.
Comprueba como compila
fun main() {
val x: Int?=nullvar y = x?.toDouble()
println(y)
y=9.8println(y)
}
Observa que la ? se puede indicar:
despues de un tipo para convertirlo en anulable y permite que una variable de ese tipo pueda tomar el valor null
despues de una variable en una expresión de la forma variable?.metodo. Si resulta que que la variable puede tomar el valor null, ocurre por tanto que la expresión en principio generaría una nullPointerException, pero el efecto de la interrogación es evitar esta excepción y que el valor que devuelva la expresión sea null
En el ejemmplo, como x es null y no especificamos el tipo de de y, el tipo se infiere de la expresión de la derecha y por tanto y se va a crear con tipo Double?, es decir admite valores Double pero también null
¿Cuál es entonces la diferencia entre x?.toDouble y x!!.toDouble()?
Si pruebas los ejemplos anteriores observas que:
x?.toDouble, si x vale nulo esta expresión devuelve null
x!!.toDouble(), si xa vale null esta expresión genera una NPE.
El operador Elvis
Se le llama operador elvis al operador ?: que nos permite completar la sintaxis de la expresión de acceso seguro para matizar, cuando nos interese, que en lugar de devolver nulo devuelva un valor concreto, por ejemplo, ahora si x vale null entonces a y le asignamos 0.0
fun main() {
val x: Int?=nullvar y = x?.toDouble()?:0.0println(y)
y=9.8println(y)
}
Si te fijas
var y = x?.toDouble()?:0.0
no es más que la forma abreviada de
var y =if (x!=null)x.toDouble() else 0.0
¡compruébalo!
Entrada de datos por teclado con readLine()
Las funciones readLine() y readln()
readline() y readln() son funciones que se pueden utilizar para leer líneas del teclado entre otras funciones. La función readln() es de existencia más reciente, aparece a partir de Kotlin 1.6, por tanto, sólo se puede utilizar con las versiones más recientes de kotlin. Para programas sencillos e iniciarse es mejor usar readln() que oculta el problema del valor null. No obstante, exponemos el mecanismo de lecturra de teclado con readLine() porque por el momento quizá nos veamos forzados a trabajar en un ordenador con versiones anteriores a 1.6 de kotlin y porque todavía se utiliza mucho en los ejemplos que consultamos en la web.
readLine() devuelve String?
readLine() devuelve una línea del teclado y la devuelve como un String al programa, pero además, readLine() también puede devolver null y por tanto decimos que readLine() devuelve un String?. Consulta los apuntes del valor null si no sabes lo que es String?
Fíjate como en la documentación oficial de kotlin nos indica que efectivamente esta función devuelve un String?
Comprueba esta afirmación observando el error de compilación del siguiente código
fun main() {
print("teclea una frase y te la repito: ")
val x: String = readLine()
print(x)
}
El problema es que por defecto en una variable de tipo String no se pueden almacenar nulos pero readLine() podría devolver null.
Usando como entrada Standard el teclado no se generan nulos pero todo esto rollazo es debido a que readLine() se puede usar con otras entradas, por ejemplo para leer ficheros.
Usar una variable que permita almacenar null.
Una solución es permitir que x almacene null usando el operador ?
fun main() {
print("teclea una frase y te la repito: ")
val x: String?= readLine()
print(x)
}
¿Qué pasa si no especificamos el tipo de una variables inicializada con readLine()?.
Observa que en el siguiente código no especificamos el tipo de x y compila correctamente
fun main() {
print("teclea una frase y te la repito: ")
val x = readLine()
print(x)
}
Para entender porqué compila, simplemente, hay que tener en cuenta que readLine() devuelve algo de tipo String? y por tanto kotlin infiere automáticamente que x es del tipo String?, es decir que
val x = readLine()
es equivalente a
val x: String? = readLine()
Esto puedo ser un buen ejemplo de que por un lado cuando no declaramos el tipo de las variables ganamos limpieza y concisión en el código pero en casos como este puede encubrir detalles importantes a tener en cuenta en el resto del código, como no ser conscientes que readLine() no devuelve String, realmente devuelve es String?
Indicar con !! que sabemos que no vamos a recibir null
ya que la entrada por teclado no genera nulos, nos podemos librar de declarar tipos con ? usando el operador !! que como ya vimos al estudiar el valor null, simplemente indica al compilador que no haga el chequeo de posible null.
fun main() {
print("teclea una frase y te la repito: ")
val x: String = readLine()!! print(x)
}
Hay muchas otras formas de afrontar el hecho de que readLine() devuelve String?. Pero por el momento usaremos la más fácil que es usar como en el ejemplo anterior el operador !!
readln() es equivalente a readLine()!!
Desde Kotlin 1.6, para hacer kotlin más fácil a los principiantes, es posible “ocultar” el problema de devolución de nulos de readLine() simplemente usando la función readln() cuyo funcionamiento es equivalente a usar readLine()!!
Fíjate como en el ejemplo anterior simplemente sustituimos readLine()!! por readln() y todo funciona igual.
fun main() {
val linea = readln()//amtes usamos readLine()!! val lista= linea.split(' ')
var suma=0
for( numero in lista){
suma=suma+ numero.toInt()
}
println("La suma es $suma")
}
más sobre colecciones básicas
Previamente comentamos aspectos fundamentales de Strings y algo de listas muy superficialmente. Añadimos ahora el estudio de las colecciones básicas de kotlin: arrays, listas, rangos y mapas. Estos recursos nos permitirá resolver nuevos retos algorítmicos que serían muy difíciles o imposibles de resolver sin estas estructuras.
Subsecciones de más sobre colecciones básicas
Arrays
Un array es un conjunto de valores del mismo tipo y permite acceder a ellos a través de una sola variable. Por ejemplo si tengo que almacenar 100 números enteros en lugar de crear 100 variables individuales puedo gestionar esos 100 valores enteros de forma simple y uniforme con un array de enteros a través de una única variable.
Otras cuestiones que caracterizan a un array son:
es una estructura de tamaño fijo, una vez que se crea no se puede modifica su tamaño.
sus elementos se almacenan de forma contigua.
a cada elemento se le asocia un índice correspondiente a su posición teniendo en cuenta que la primera posición se corresponde con el índice 0.
Por ejemplo, esto sería la visión gráfica de un array de 6 elementos de tipo Char
La potencia de objetos como el array se entenderá cuando estudiemos estructuras de control. Vemos en este documentos sólo algunas de las cuestiones básicas del manejo de arrays
Crear un Array
Hay varias formas de crear una array, para simplificar, por el momento nos limitamos a crear un array de dos formas:
con arrayOf()
con arrayOfNulls()
Crear un array con arrayOf()
Simplemente separamos por comas una lista de los elementos que va a almacenar el array. Recuerda que todos los elementos tienen que ser del mismo tipo. El tamaño del array se deduce automáticamente del tamaño de la lista indicada.
val nombres= arrayOf("yo","tú","el")//array de stringsval impares= arrayOf(1,3,5)//array de enteros
Crear una array con arrayOfNulls()
Puede ocurrir que al principio del programa no sepamos los valores del array ya que por ejemplo se quieren introducir los valores por teclado, en este caso, podemos crear el array de forma que contenga sus elementos inicializados al valor null. Basta en este caso simplemente indicar el tamaño deseado y el tipo de los elementos.
val impares= arrayOfNulls<Int>(3)
Tamaño de un array
Ya indicamos que el tamaño o longitud del array se determina en el momento de su creación.Podemos consultar el tamaño de un array a través de la propiedad size
fun main() {
val nombres= arrayOf("yo","tú","el")
val impares= arrayOf(1,3,5,7)
println("tamaño de array nombres:"+ nombres.size)
println("tamaño de array impares:"+ impares.size)
}
Acceder a un elemento del array
Los arrays son accesibles con un sistema de índices similar al que vimos con los Strings de forma que el primer elemento se corresponde con el índice 0, el segundo con el 1, etc. Al igual que con los Strings el acceso a los elementos del array se realiza indicando el índice entre corchetes.
Por ejemplo, comprobamos que efectivamente con arrayOfNulls inicializamos a Null y a continuación cambiamos de valor los elementos del array
fun main() {
val impares= arrayOfNulls<Int>(3)
println(impares[0])
println(impares[1])
println(impares[2])
impares[0]=55
impares[1]=99
impares[2]=33
println(impares[0])
println(impares[1])
println(impares[2])
}
Los arrays tienen un gran número de cuestiones que tratar, no obstante, ya que esto es un curso introductorio preferimos por diversas razones utilizar listas en lugar de arrays. A continuación estudiaremos el concepto de lista en Kotlin.
Listas
Una lista, de forma simplificada, es una evolución mejorada de un array.
La característica más básica de un array es su acceso por posición a cada elemento individual a traves de [] y esta característica también es posible trabajando con listas como vemos en el siguiente ejemplo
fun main() {
//val miLista: List<Int> = listOf(1, 2, 3)//el tipo en este caso lo puede inferir el compilador val miLista = listOf(1, 2, 3)
println("imprimir toda la lista junta $miLista") // [1, 2, 3] println("imprimir la lista elemento a elemento ")
println(miLista[0])
println(miLista[1])
println(miLista[2])
println("el tamaño de la lista es: "+ miLista.size)
}
Algunas diferencias importantes entre listas y arrays
Hay dos tipos básicos de listas:
inmutables
mutables. Permiten modificar el valor de sus elementos así como añadir/borrar elementos a la lista, es decir, modificar el tamaño de la lista.
Un array es una mezcla de los comportamientos anteriores. Su tamaño se fija en el momento de su creación y no se puede cambiar pero cada elemento individual puede cambiar en cualquier momento.
El ejemplo anterior de listas se corresponde con una lista inmutable, para lo cual utilizamos la función listOf(). A continuación veremos un ejemplo de lista inmutable.
Ejemplo de lista mutable
Hay varias formas de crear una lista mutable, Vemos un ejemplo con mutableListOf()
fun main() {
val colorsList = mutableListOf("Amarillo", "Azul", "Rojo")
colorsList.add("Verde") // [Amarillo, Azul, Rojo, Verde] //inserta al final colorsList.add(0, "Blanco") // [Blanco, Amarillo, Azul, Rojo, Verde]//inserta en la posición indicada indicada colorsList.removeAt(2) // [Blanco, Amarillo, Rojo, Verde]//observa como modificamos con [] colorsList[1]="Negro"// [Blanco, Negro, Rojo, Verde] println(colorsList)
println(colorsList[0])
}
declarar una lista mutable de tamaño 0 (vacía)
Podemos querer ir construyendo una lista partiendo de una lista vacia. Al partir de una lista vacía Kotlin no puede inferir el tipo de la lista. La solución es incluir el tipo en la declaración de la lista de alguna manera como en el ejemplo.
fun main(){
var lista= mutableListOf<Int>()//lista de Int de tamaño 0 println(" tamaño lista ${lista.size}")
lista.add(99)
println(" tamaño lista ${lista.size}")
}
Listas vs Arrays
Se prefieren las listas. Las listas tienen características actualizadas de seguridad y permiten una programación más cómoda y legible. Entonces, ¿porqué existen arrays en kotlin?
Los arrays pueden ser más eficientes. los arrays garantizan un almacenamiento de los datos de forma contigua en memoria. Esto los hace más eficientes pero hoy en día esto sólo tiene impacto en aplicaciones muy concretas.
Kotlin es compatible con Java. En java los arrays son muy importantes.
La funcion split() de los Strings
split() permite trocear o dividir un String en trocitos más pequeños y estos trozos los devuelve en un lista. Como parámetro se le indica el criterio de división o delimitador.
Por ejemplo el delimitador en el siguiente ejemplo es el String “:”
fun main() {
val str ="A:B:C:que bonito:z zz" val delim =":" val list = str.split(delim)
println(list) // [A, B, C, que bonito, z zz]}
El delimitador realmente es una expresión regular pero de momento con pensar que es un caracter no es suficiente.
Uno de los usos más frecuentes es querer dividir un texto en palabras utilizando como delimitador el espacio en blanco.
fun main() {
val str ="Había una vez un circo que alegraba siempre la ilusión" val delim =" " val list = str.split(delim)
println(list) // [Había, una, vez, un, circo, que, alegraba, siempre, la, ilusión]}
Utilizaremos split() para com combinar con el readln() para conseguir un estilo de entrada de datos por teclado que veremos más adelante.
Asignaciones entre variables Lista
Se explica este concepto con variables tipo lista pero es igualmente aplicable a variables tipo array.
Una variable Lista no almacena directamente los datos de la lista si no que almacena la dirección de memoria donde están almacenados los datos
fun main() {
var a = mutableListOf(0,2,4,6,8)
var b = mutableListOf(1,3,5,7,9)
println(a)
println(b)
}
La situación en memoría podemos imaginarla como:
Observa ahora la aparición de una nueva variable c
fun main() {
var a = mutableListOf(0,2,4,6,8)
var b = mutableListOf(1,3,5,7,9)
var c =a
a=b
println(a)
println(b)
println(c)
}
La situación en memoría podemos imaginarla como:
Conclusión: Una asignación entre variables lista no provoca que se copie la lista, si no que la variable de la izquierda también referencia a la misma lista que la variable de la derecha.
Ordenar Listas
Es muy habitual tener una lista y querer ordenarla. Ordenar una lista es un tema más complejo de lo que aparenta y nosotros por el momento nos limitamos a ordenar listas de tipos básicos como Int y Strings de forma ascendente/Descente por “su orden natural”. Para este ordenamiento básico podemos usar la función:
sort()/sortDescending(). Entonces el orden se aplica sobre la lista original
sorted()/sortedDescending(). Entonces la función devuelve una nueva lista ordenada.l.
//ejemplo con sorted()fun main(){
val lista= listOf(4,2,99,7,12)
var listaOrdenada=lista.sorted()
println(" lista de enteros ordenada $listaOrdenada")
println(" lista de enteros antigua está sin ordenadar $lista")
listaOrdenada=lista.sortedDescending()
println(listaOrdenada)
val listaStringsOrdenados= listOf("zalamero","cielo","azul").sorted()
println(listaStringsOrdenados)
println(listaStringsOrdenados.sortedDescending())
}
//ejemplo con sort()fun main(){
//val lista= listOf(4,2,99,7,12) // para sort() tiene que ser mutable val lista= mutableListOf(4,2,99,7,12)
lista.sort()
println(lista)
}
Listas de dos dimensiones
Hasta ahora trabajamos con Listas/Arrays unidimensionales y accedíamos a sus elementos a través de un índice.
Una lista de dos dimensiones se puede crear mediante la creación de una lista de listas. Por ejemplo:
En este ejemplo, hemos creado una lista de tres elementos, donde cada elemento es una lista de tres números enteros. Esta estructura se puede visualizar como una matriz de 3 filas y 3 columnas, con los siguientes valores:
1 2 3
4 5 6
7 8 9
Por lo tanto, aunque realmente una lista de listas consiste en y se almacena como una lista donde cada uno de sus elementos es a su vez otra lista, para resolver muchos problemas de programación es más conveniente visualizar la lista de listas como una tabla y acceder a cada elemento con dos índices teniendo en cuenta que el primer índice representa la fila de la tabla y el segundo índice representa a la columna de la tabla.
En el siguiente ejemplo imprimimos la diagonal de la tabla
fun main(){
val lista2D = listOf(
listOf(1, 2, 3),
listOf(4, 5, 6),
listOf(7, 8, 9)
)
val fila0Col1 = lista2D[0][0]// Devuelve 1 val fila1Col1 = lista2D[1][1]// Devuelve 5 val fila2Col2 = lista2D[2][2]// Devuelve 9 println("$fila0Col1 , $fila1Col1 , $fila2Col2")
}
Listas de dos dimensiones mutables
Crear una lista de dos dimensiones vacia y que vaya aumentando de tamaño a medida que lo necesitemos.
fun main() {
val tablero = mutableListOf<MutableList<String>>()
var fila= mutableListOf("00","01","02")
tablero.add(fila)
fila= mutableListOf("11","11","12")
tablero.add(fila)
println(tablero)
println("-----------")
println(tablero[0])
println(tablero[1])
println("-----------")
tablero[0][0]="99" tablero[1][2]="88" println(tablero[0])
println(tablero[1])
println(tablero[1][1])
println("-----------")
tablero[0].add("03")
println(tablero)
}
listas de más de dos dimensiones
Las ideas aquí explicadas se pueden extender a cualquier número de dimensiones. Por lo tanto, al respecto de número de dimensiones podemos clasificar las listas/arrays en:
unidimensionales (se definen con una dimensión)
multidimensionales (se definen con más de una dimensión)
Rangos
Un rango en Kotlin es tipo que engloba un conjunto de valores que representa el concepto matemático de intervalo de valores. Es decir, es un subconjunto de elementos comprendidos entre un extremo inferior a y un extremo superior b.
Por ejemplo, el rango [0,5] representa los valores enteros del 0 al 5. Para crearlo se usa la función operador toRange()
fun main() {
val fromZeroToFive = 0.rangeTo(5)
println(fromZeroToFive) // 0..5}
Otra sintaxis alternativa y muy usada en la práctica es el formato (a..b) .
fun main() {
val fromZeroToFive = 0..5println(fromZeroToFive) // 0..5}
Mapas
El mapa de Kotlin es una colección de pares clave/valor, donde cada clave es única y solo se puede asociar con un valor. Sin embargo, el mismo valor se puede asociar con varias claves. Podemos pensar en un mapa como la típica tabla en la que la primera columna almacena claves y la segunda columna los valores asociados a las claves. Cada fila refleja la asociación entre una clave y un valor.
En el siguiente ejemplo represento un mapa con clave telefono y valor nombre. Observa que las claves són únicas pero los nombres no tienen porqué.
Telefono
Nombre
111
Pepe
222
Julieta
333
Romeo
444
Pepe
555
Chuly
Un mapa de Kotlin puede ser mutable ( mutableMapOf ) o de solo lectura ( mapOf ).
Los mapas también se conocen como diccionarios o matrices asociativas en otros lenguajes de programación. Por ejemplo en Python a los mapas se les llama diccionarios.
Crear mapas
Para crear un mapa hay multiples sintácticas. La más sencilla es con mapOf para un mapa inmutable y mutableMapOf para mutable y en ambos casos relacionando cada clave con su valor con el operador to
fun main() {
val miMapaInmutable = mapOf(111 to "Pepe",222 to "Julieta",333 to "Romeo",444 to "Pepe",555 to "Chuly")
println(miMapaInmutable)
val miMapaMutable = mutableMapOf(111 to "Pepe",222 to "Julieta",333 to "Romeo",444 to "Pepe",555 to "Chuly")
println(miMapaMutable)
}
Acceder por clave a un valor
la operación más importante al trabajar con un mapa es dada una clave acceder a su valor. Si la clave proporcionada no existe el valor asociado será null. La clave se puede especificar con el metodo get() o con [].
fun main() {
val miMapa = mapOf(111 to "Pepe",222 to "Julieta",333 to "Romeo",444 to "Pepe",555 to "Chuly")
var valorde111= miMapa[111]println(valorde111)
valorde111=miMapa.get(111)
println(valorde111)
println(miMapa[9999])//no existe esta clave val mapaMutable= mutableMapOf(111 to "PepeMutable",222 to "JulietaMutable")
valorde111=mapaMutable[111]println(valorde111)
valorde111=mapaMutable.get(111)
println(valorde111)
}
Modificar un valor en mapa mutable
Simplemente asignamos el nuevo valor a la clave
fun main() {
val m= mutableMapOf(111 to "Yo",222 to "XX")
println(m)
m[222]="Tú" println(m)
}
Añadir un valor a un mapa mutable
Si al usar el operador de asignación como en el caso anterior, la clave no existe se asume que queremos insertar un nuevo elemento.
fun main() {
val m= mutableMapOf(111 to "Yo",222 to "Tú")
println(m)
m[333]="El" println(m)
}
Partir de un mapa mutable vacío e ir añadiendo elementos
fun main() {
val m= mutableMapOf<Int, String>()
println(m)
m[111]="Pepe" m[343]="chuly" println(m)
}
Iterar sobre colecciones
Es un tema muy extenso con muchos matices y posibilidades que se irán incorporando y analizando a lo largo del curso. Por el momento vemos las posibilidades más básicas. Sobre strings y rangos ya las habíamos visto previamente para las incorporamos también aquí por completitud.
recuerda la sintaxis del bucle for
for (item in colección) {
// cuerpo del bucle
}
como ves el for en kotlin es una estructura pensada directamente para iterar sobre colecciones y será la que analizaremos en este documento. También puede iterarse con un bucle while a través del manejo de índices pero suele preferirse con for.
Iterar sobre un String
En el siguiente ejemplo, en cada iteración se imprime una letra de la palabra “hola”
fun main() {
for (item in "hola") {
println(item)
}
}
Iterar sobre un rango
fun main() {
for (item in 1..5) {
println(item)
}
}
El desplazamiento a través del rango es de uno en uno, es decir, en cada paso incremento el desplazamiento dentro del rango en 1. Con la palabra reservada step puedo indicar otro incremento de desplazamiento.
fun main() {
for (item in 1..10 step 2) {
println(item)
}
}
Puede querer iterar sobre un rango pero comenzado por el último elemento y avanzando descendentemente utilizando la palabra reservada downTo
fun main() {
for (item in 5 downTo 1 step 2) {
println(item)
}
}
Iterar sobre un array
Es raro ver un array y que de alguna manera no sea manipulado por un bucle. Recorrer un array es una operación muy frecuente.
Iterar sin preocuparnos de los límtes del array
La forma más sencilla de recorrer un array de principio a fin consiste simplemente en dejar que el operador in detecte automáticamente el fin del array.
fun main() {
var fruits = arrayOf("Orange", "Apple", "Mango", "Banana")
for (item in fruits) {
println(item)
}
}
Es un método de recorrido limpio y cómodo pero sólo vale para cuando queremos leer el contenido del array de principio a fin. Observa que por ejemplo, si a medida que recorremos el array queremos modificarlo no es posible de una manera sencilla y directa ya que la forma fácil de modificar un array es
array[pos]=valor
es decir, necesitamos el índice de posición para modificar.
Iterar basándonos en un rango de índices
Para multitud de situaciones algorítmicas, vamos a necesitar de alguna manera manejar los índices del array para recorrerlo. Por el momento fíjate simplemente en diversas formas de recorrer con índices basándonos en un rango. Ya irás descubriendo la potencia e importancia de trabajar con índices sobre un array poco a poco.
En el rango indicamos el índice en que queremos comenzar y con el que queremos acabar. Un comienzo muy habitual es el índice 0 y un final muy habitual es lastIndex, o expresado de otra forma, size-1 pero no tienen que ser estos obligatoriamente.
fun main() {
var fruits = arrayOf("Orange", "Apple", "Mango", "Banana")
for (index in 0..fruits.lastIndex) {
print(fruits[index]+" ")
}
println()
//idem con size-1for (index in 0..fruits.size-1) {
print(fruits[index]+" ")
}
}
Si el rango que indicamos va desde 0 hasta el último como en los ejemplos anterior, tenemos la posibilidad de acceder a este rango a través de la propiedad indices
fun main() {
var fruits = arrayOf("Orange", "Apple", "Mango", "Banana")
for (index in fruits.indices) {
println(fruits[index])
}
}
Iterar sobre una lista
En lo básico, idem que lo visto para arrays. En el siguiente ejemplo simplemente sustituimos en el ejemplo anterior arrayOf() por listOf
fun main() {
var fruits = listOf("Orange", "Apple", "Mango", "Banana")
for (index in fruits.indices) {
println(fruits[index])
}
}
Iterar sobre un mapa
Hay mil formas, pero la más sencilla consiste en utilizar un par de variables de la forma (x,y) de forma que en cada iteración x reciba la clave e y el valor como en el siguiente ejemplo.
fun main() {
val miMapa = mapOf(111 to "Pepe",222 to "Julieta",333 to "Romeo",444 to "Pepe",555 to "Chuly")
//recorrer el mapa con propiedad key y valuefor ((key, value) in miMapa) {
println("Clave: $key Valor: $value")
}
}
Más adelante, estudiaremos más en profundidad la desestructuración kotlin. La desestructuración en Kotlin es una característica que permite descomponer una estructura de datos en partes individuales y asignarlas a variables separadas en una sola declaración como hicimos en el ejemplo de arriba.
Programación orientada a objetos
Estudiamos los fundamentos de la programación orientada a objetos sin entrar en detalles de diseño y patrones.
Nos centramos sobre todo en aspectos sintácticos y en esta primera versión de los apuntes se asume que el alumno tiene conocimientos de POO en java
Subsecciones de Programación orientada a objetos
Objetos y clases
Objetos y Clases
En kotlin todo es un objeto, los datos Int realmente son objetos, las
funciones son objetos, etc. Por lo tanto, ya estuvimos utilizando
objetos, objetos que pertenecen a clases del sistema Kotlin. En este
cuaderno nos introducimos al uso de objetos que son instancias de clases
escritas por nosotros mismos.
Una primera visión de lo que es un objeto
Los objetos almacenan datos usando para ello propiedades val/var y
pueden contener en su interior definición de operaciones que
probablemente utilizan los datos anteriores para hacer algo.
Algunas definiciones para ir arrancando:
Una clase: Define en su interior propiedades y funciones. A las
clases también se les llama tipos datos definidos por el usuario.
Así tenemos los tipos propios del lenguaje como Int y String y los
que va definiendo en sus aplicaciones el usuario como la clase
Coche, Persona etc..
Miembro: Una clase contiene en su interior miembros. Hay dos tipos
principales de miembros a los que ya aludimos: propiedades y y
funciones.
En kotlin una función se puede definir dentro de una clase o fuera
de toda clase y según este criterio hay dos tipos de funciones
funciones top-level. Las que se escriben directamente en el
fichero "al top level del fichero" fuera de toda clase.
funciones miembro. Se escriben dentro de una clase y su
funcionamiento está ligado a un objeto específico de la clase.
Crear un objeto: Como punto de partida y sin ningún rigor digamos que crear un objeto consiste en hacer un val o var al nombre de una clase. A los objetos también se les llama instancia de una clase.
DEFINICIÓN DE UNA CLASE
En general la fexibilidad de Kotlin redunda en multitud de posibilidades
sintácticas para escribir lo mismo. Aquí recogemos sólo las
posibilidades más fundamentales.
Una clase es una plantilla o modelo que se utilizará para crear objetos.
Una clase de Kotlin se define usando la palabra clave class. El cuerpo
de una clase puede contener propiedades y/o funciones miembro. En otro
cuaderno afinaremos el concepto de propiedad (property). Como punto de
partida podemos ver una propiedad como una variable que pertenece a la
clase.
La declaración básica de un clase consta del nombre de la clase y el
cuerpo de la clase rodeado de llaves.
classPersona{
var nombre =""var edad = 0
fun printMe() {
print(nombre+" "+ edad)
}
}
Tanto el encabezado como el cuerpo son opcionales; si la clase no tiene
cuerpo, se pueden omitir las llaves. La siguiente es una declaración
válida de clase. Una clase vacia que se define sólo con la palabra
reservada class seguida de un nombre.
class miClaseVacia
CREACIÓN DE OBJETOS
Los objetos se crean a partir una clase que funciona a modo de plantilla
o molde. "El molde" describe propiedades y comportamientos. Por lo
tanto, todos los objetos de una clase tendrán la misma estructura de
propiedades y comportamientos.
La sintaxis más básica para crear un objeto de una clase es:
var varName = ClassName()
Podemos acceder a las propiedades y métodos de una clase usando el
operador punto "."
var varName = ClassName()
varName.property = valor
varName.functionName()
En el siguiente ejemplo creamos un objeto de la clase Persona y
utilizamos sus miembros con el operador .(punto)
classPersona{
var nombre =""var edad = 0
fun printMe() {
print(nombre+" "+ edad)
}
}
val p = Persona()
p.nombre="yo"p.edad=14
p.printMe()
Podemos crear objetos de una clase vacía aunque raramente esto tenga utilidad
classMiClasefun main(){
val x= MiClase()
println(x)
}
La referencia this
this no es más que una variable automáticamente creada por el sistema para cada objeto y que se utiliza para referenciar al propio objeto dentro del propio objeto. Se puede usar la variable this para referenciar a los miembros de un objeto desde dentro de ese objeto. El siguiente ejemplo es equivalente al anterior, la única diferencia es que desde printMe() para aludir a los propiedades también usamos la referencia this.
classPersona{
var nombre =""var edad = 0
fun printMe() {
print(this.nombre+" "+this.edad)
}
}
val p = Persona()
p.nombre="yo"p.edad=14
p.printMe()
Usar o no usar this
Los dos últimos ejemplos son equivalentes, entonces, ¿se debe usar this o no?. Es una cuestión de estilo, y en el caso del lenguaje Kotlin, cuando this no es necesario, se prefiereno usarlo en aras de la limpieza y concisión objetivo del estilo kotlin.
Y dicho esto, hay diversas situaciones en que el uso de this es necesario y las iremos examinando a medida que lo requiramos.
Constructores
Constructores Kotlin
Un constructor es una función miembro especial que se invoca
automáticamente cuando se crea un objeto de la clase. Su objetivo
principal es inicializar propiedades y otras variables. Una clase debe
tener un constructor y, si no declaramos ningún constructor, el
compilador genera un constructor predeterminado.
Kotlin tiene dos tipos de constructores:
Constructor principal o primario
Constructor secundario
Una clase en Kotlin puede tener como máximo un constructor principal y
uno o más constructores secundarios. El constructor principal se utiliza
cuando simplemente queremos hacer asignaciones de valores a las
propiedades. El constructor secundario se usa cuando se requiere hacer
las inicializaciones con algo de lógica adicional.
El constructor predeterminado
En el ejemplo que sigue, la clase Persona no define ningún constructor y
por tanto se puede utilizar el constructor predeterminado para crear un
objeto de esa clase. El constructor predeterminado se invoca con el
nombre de la clase seguido de parentesis vacios.
classPersona{
val nombre:String="yo" val edad:Int=22
}
val p=Persona()//usando constructor predeterminadoprint("soy ${p.nombre} y tengo ${p.edad} años")
El constructor principal
Hay unas cuantas variantes sintácticas a la hora de escribir un
constructor principal. Nos centramos en lo más básico.
El constructor principal se escribe simplemente indicando despues del
nombre de la clase la palabra clave constructor, y a continuación
entre paréntesis, un conjunto de parámetros separados por comas.
Lo más habitual y simple es que los parámetros sean variables. La
definición de las variables puede hacerse con val/var o sin val/var.
Si queremos que los parámetros definan al mismo tiempo propiedades es
necesario hacerlo con var/val Es decir, en este caso el uso de
var/val convierte a un parámetro variable en propiedad de la clase.
Los parámetros también pueden ser funciones pero no ejemplificamos por
el momento este caso para simplificar.
classPersonaconstructor(var nombre:String, var edad:Int){
//nombre y edad son propiedades definidas en el constructor principalvar email="chuchy@gmail.com"//propiedad no definida en constructor principal fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",15)
println(p1.nombre)//nombre es una propiedad y utilizamos el punto para acceder a ellaprintln(p1.email)//email también es una propiedad aunque se declaro fuera del constructor principalp1.printMe()
val p2 = Persona("tu",35)
p2.
p2.printMe()
La palabra clave constructor se puede omitir si no hay anotaciones o
modificadores de acceso especificados como público, privado o protegido.
classPersona (var nombre:String, var edad:Int){
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",15)
p1.printMe()
val p2 = Persona("tu",35)
p2.printMe()
Se pueden inicializar los parámetros del constructor con valores
predeterminados.
classPersona (var nombre:String, var edad:Int=90){
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo")
p1.printMe()
val p2 = Persona("tu",35)
p2.printMe()
parametros que no son propiedades
Sí no indicamos en los paréntesis del constructor los parámetros con
val/var entonces es que preferimos definir dentro de la clase las
propiedades y entre los paréntesis simplemente se indican variables que
no tienen el rango de propiedades de la clase.
classPersona (elnombre:String, laedad:Int){
var nombre=elnombre
var edad=laedad
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",15)
p1.printMe()
val p2 = Persona("tu",35)
p2.printMe()
Bloques init
El constructor principal simplemente tiene capacidad para hacer
asignaciones de valores a variables. Podemos añadir lógica a la
inicialización con un bloque init. Puede haber más de un bloque de
inicialización durante la inicialización de una instancia, los bloques
de inicialización se ejecutan en el mismo orden en que aparecen en el
cuerpo de la clase.
classPersona (var nombre:String, var edad:Int){
init{
if(edad<0) edad=0
}
init{
println("Segundo init")
}
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",-15)
p1.printMe()
val p2 = Persona("tu",35)
p2.printMe()
un uso típico de this
Indicamos que progresivamente iran saliendo casos que precisan el uso de
this. Un caso muy habitual es que coincidan los nombres de los
parámetros con los nombres de las propiedades. En este caso, el nombre
del parámetro oculta al de la propiedad por lo que para referirnos a la
propiedad debemos de utilizar la palabra reserva this.
En el siguiente ejemplo decidimos definir las propiedades dentro de la
clase, no através de los parámetros. Observa que los parámetros del
constructor no llevan var/val. Además, también a proposito, decidimos
que coincidan los nombres de los parámetros y las propiedades.
Inevitablemente, necesitamos usar this para referirnos a las
propiedades.
classPersona (nombre:String,edad:Int){
var nombre:String
var edad:Int
init{
this.nombre=nombre
this.edad=edad
if(this.edad<0) this.edad=0
}
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",-15)
p1.printMe()
val p2 = Persona("tu",35)
p2.printMe()
Puedes comprobar modificando el ejemplo de arriba que los
parámetros(variables sin indicar var/val) son realmente variables val,
de forma que si queremos cambiar su valor dentro de la clase no es
posible.
Otra posiblilidad sintáctica consiste en inicializar las propiedades con
los parámetros justo en el momento de declarlos, en este caso no se
puede usar this, al ir el nombre de la propiedad detrás de var no hay
ambigüedad en el contexto de que es propiedad y que es parámetro.
classPersona (nombre:String,edad:Int){
var nombre:String=nombre//no hay ambigüedad, no hace falta this (ni se puede indicar)var edad:Int=edad//no hay ambigüedad, no hace falta this init{
if(this.edad<0) this.edad=0
}
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",-15)
p1.printMe()
val p2 = Persona("tu",35)
p2.printMe()
Constructores secundarios
Recuerda que el constructor primario se declaraba en la cabecera de la
clase con la palabra reservada constructor(opcional en ciertas
situaciones). Un constructor secundario también se declara con la
palabra reservada constructor pero se hace dentro del cuerpo de la
clase.
En el siguiente ejemplo no hay constructor primario pero hay un
constructor secundario.
classPersona{
var nombre:String
var edad:Int
constructor(nombre:String, edad:Int){
this.nombre=nombre
this.edad=edad
}
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",15)
p1.printMe()
val p2 = Persona("tu",35)
p2.printMe()
Puede haber varios constructores secundarios
es un efecto similar a la sobrecarga de funciones
classPersona{
var nombre="sin nombre"var edad:Int
constructor(edad:Int){
this.edad=edad
}
constructor(nombre:String, edad:Int){
this.nombre=nombre
this.edad=edad
}
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",15)
p1.printMe()
val p2 = Persona(35)
p2.printMe()
puedem coexistir simultáneamente constructor primario con secundarios
En este caso es obligatorio usar la expresión this en la cabecera del
secundario para delegarle los parámetros que requiera.
classPersona(var nombre:String){
var edad:Int
init{
edad=99
}
constructor(nombre:String, edad:Int):this(nombre) {
this.edad=edad
}
fun printMe() {
println(nombre+" "+ edad)
}
}
val p1 = Persona("yo",15)
p1.printMe()
val p2 = Persona("tu")
p2.printMe()
Propiedades
PROPIEDADES EN KOTLN
¿Qué es una propiedad en kotlin?
En la POO tradicional el término propiedad se refiere a lo que conocemos
por “atributo” , “field” o “campo”. En kotlin es un concepto un poco más
amplio pues engloba: un campo, su función accesor get y su función
mutador set. Los conceptos de get y set son idénticos en Kotlin que en
c++ o Java, en kotlin simplemente se hace más automática sintácticamente
la relación entre un campo y sus típicos get/set
accesores: getters y setters
Vimos en ejemplos previos que para declarar una propiedad simplemente
usábamos var/val como para las variables locales de las funciones. Si
una variable con var/val se define en el cuerpo de la clase fuera de
toda función o en el constructor primario ya se constituye en propiedad
de la clase. Pero realmente una propiedad kotlin es algo más. Esta es la
sintáxis completa de declaración de una propiedad:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
Fíjate que en la sintaxis anterior, que en la definición de la propiedad
si queremos podemos incluir un get/set asociado a la propiedad de forma
muy concisa y compacta Veamos un ejemplo que incluye los get y set
classPersona {
var nombre:String ="chuly"// getter get() =field +" Soy get()"// setter set(value) {
field = value +" metido por set" }
}
fun main(){
val p= Persona()
println(p.nombre)
p.nombre="rosky" println(p.nombre)
}
la keyword field y el concepto de back field en kotlin
Simplificadamente una propiedad podemos resumirla con la siguiente fórmula
*propiedad = valor + set +get*.
Así que en kotlin una propiedad es algo más que un valor. Internamente, de alguna manera se tiene que
almacenar este valor y esto se hace a través una variable interna que se llama back field, Al escribir los métodos get/set a menudo querremos acceder a este valor y esto se hace con la palabra reservada
field. Es decir, al back field se accede con la palabra reservada field. Lógicamente esta palabra sólo tiene sentido dentro de la declaración de una propiedad.
Si en el ejemplo anterio en lugar de field hubieramos usado el nombre de
la propiedad se habría producido un efecto recursivo indeseado.
Accesores Por Defecto
Si al declarar una propiedad no especificamos accesores, kotlin crea
unos por defecto. Por ejemplo:
classPersona{
var nombre ="chosky" }
Equivale a definir:
classPersona{
var nombre ="chosky" get() = field
set(value) {
field = value
}
}
A menudo los accesores por defecto son más que suficientes y no
necesitamos por lo tanto escribirlos salvo que precisemos
personalizarlos.
relación entre var/val y accesores
Recuerda que val genera variables no modificables, por tanto, cuando
declaras una propiedad con val, solo vamos a poder personalizar el get
ya que el set no es accesible. Por tanto:
val => campo + get
var => campo + get + set
Visibilidad de accesores
Más adelante estudiaremos los modificadores de visibilidad con más
detenimiento. Por el momento observamos que uno de esos modificadores es
private. Se puede aplicar private a un set para prohibir su uso
fuera de la clase.
classPersona{
var nombre ="chosky"private set
}
fun main(){
val p=Persona()
println(p.nombre)
p.nombre="Rusky"//error set es private}
En el siguiente ejemplo vemos un caso de porqué puede resultar interesante hacer private el set de una propiedad.
Tenemos una clase que encapsula unas coordenadas x, y. No queremos definir como val las propiedades porque queremos poder cambiar su valor, pero por otro lado, no queremos poder cambiar directamente las coordenadas en una instrucción de asignación ya que queremos forzar a usar las funciones miembro de la clase para cambiar las coordenadas.
classCoordenadas {
var x: Int = 0
private set
var y: Int = 0
private set
fun moveLeft() {
x -=if (x == 0) 0 else 1
}
fun moveRight() {
x +=if (x == 300) 0 else 1
}
fun moveUp() {
y -=if (y == 0) 0 else 1
}
fun moveDown() {
y +=if (y == 300) 0 else 1
}
}
fun main(){
val c= Coordenadas()
c.moveLeft()
//c.x=77//error}
Ten claro que main() no puede usar el set() de por ejemplo x por ser private, pero que por ejemplo, moveLeft() si puede acceder al set() de x aun siendo private ya que moveLeft() es una función miembro de la clase Coordenadas.
campos calculados.
En lenguajes como java o c++ no solía quererse definir un atributo que
su valor dependía de otros atributos. Por ejemplo el área de un
rectangulo depende de el ancho y alto, si cambia el ancho y el alto
cambia el área, por esta razón en estos lenguajes se prefiere usar un
método/funcion area() que calcule el valor del área para evitar
almacenar el valor del área, simplemente, cada vez que se requiera se
calcula invocando al método/función.
En kotlin en cambio si que tiene sentido definir una propiedad área quer
realmente encapsula un método que hace el cálculo pero el resultado
final es que obtenemos un objeto con una riqueza semántica al objeto que
hace que se asemaje más a los objetos de la realidad, ya que,
efectivamente en la realidad nos gusta ver el area como una propiedad de
un rectángulo, no sólo como un cálculo.
classRectangle(val width: Int, val height: Int) {
val area: Int // property type is optional since it can be inferred from the getter's return type get() =this.width*this.height}
fun main(){
val mirectangulo= Rectangle(2,3)
print(mirectangulo.area)
}
Backing properties
La keyword field sólo es posible usarla dentro de los accesores get/set. Por lo tanto, ya que solo los get/set son capaces de de usar field sólo los get/set son capaces de acceder directamente al valor de una propiedad. Ni siquiera otros métodos de la propia clase pueden acceder directamente al valor, se ven obligados a acceder a través de
los get/set.
Observa el siguiente ejemplo. Tienes que tener claro que imprimirNombre() no está usando el field de nombre, está usando el get() de nombre. Esto realmente ya fue discutido más arriba en este documento.
classPersona {
var nombre ="chuly"// getter get() =field +" Soy get()" fun imprimirNombre() = println(nombre)
}
fun main() {
val persona = Persona()
persona.imprimirNombre()
}
Usar una back property asociada a una property
Si dentro de una clase escribimos una serie de funciones miembro, es habitual querer que dichos métodos puedan acceder al valor de una propiedad “saltándose el filtro” de set/get", ya que dicho filtro está pensando a menudo para las funciones externas, pero no tienen sentido para las funciones miembro.
Para conseguir que las funciones miembros “se salten los filtros set/get”, podemos usar una un segunda propiedad de respaldo de la primera que llamamos Back property. Usaremos está back property para trabajar de forma asociada
con la propiedada a la que respalda. Por convenio a una Back property debemos declararla con un nombre igual que el de la propiedad a la que queremos respaldar pero comenzando con un guión bajo. El guión bajo es una norma de estilo que advierte que no se debe acceder a esta propiedad desde fuera de la clase pero no lo evita. Si queremos que el acceso no se produzca debemos además de añadir el modificador de visibilidad private.
classPersona {
privatevar _nombre="chuly"//_nombre va a funcionar como propiedad de respaldo a nombrevar nombre:String
// getter get() =_nombre +" Soy get()"//setter set(value){
_nombre=value.uppercase() //con set() obligamos a almacenar en mayúsculas }
fun imprimirNombre() = println(_nombre) //evitamos imprimir al final "soy get()"}
fun main(){
val p= Persona()
p.imprimirNombre()
println(p.nombre)
//println(p._nombre) //¡ERROR! p.nombre="Zurky" p.imprimirNombre()
println(p.nombre)
}
Si observas el ejemplo de arriba, cuando trabajamos con un propiedad de respaldo ¿Quién va a acceder a dicha propiedad?:
las funciones miembro de la clase que quieren acceder a un valor “original” sin filtros get/set
los set/get de la propiedad asociada para mantener la lógica de valor original y valor filtrado. En muchas situaciones al trabajar con back property los set/get usarán esta back property en lugar de field. Todo depende de la lógica deseada y recuerda que cuando “se mete la lógica por medio” suele haber muchas soluciones o enfoques
equivalentes.
Pese a toda la discusión anterior al respecto de back property, en general, no necesitaremos usar back properties salvo para casos bastante poco frecuentes. Para casos normales que tienen su equivalente usando field es una complicación innecesaria.
Sobreescribir toString()
Sobreescribir toString()
Entenderemos con detalle que signfica override al estudiar herencia.
Manejamos ahora intuitivamente este concepto para utilizar la función
toString() ya desde nuestros primeros ejercicios de clases con kotlin.
toString() es una función miembro cuyo objetivo es ofrecer una
representación textual del estado de un objeto. Si sobreescribimos
toString() podemos personalizar dicha representación textual. Debemos
escribir la función toString() con el modificador override para tener
una representación textual del objeto a nuestro gusto.
classRectangle(val width: Int, val height: Int) {
val area: Int // property type is optional since it can be inferred from the getter's return type get() =this.width*this.height override fun toString():String{
return"ancho: $width alto: $height area: $area" }
}
val mirectangulo= Rectangle(2,3)
println(mirectangulo.area)
println(mirectangulo)
modificadores de visibilidad
Los modificadores de visibilidad se utilizan para controlar la visibilidad de una clase, sus miembros (propiedades, funciones y clases anidadas) y sus constructores.
Hay cuatro modificadores de visibilidad en Kotlin: private, protected, internaly public. La visibilidad predeterminada es public.
##Modificador public
A diferencia de Java, en Kotlin no hay necesidad de declarar nada como public – es el modificador predeterminado.
En el siguiente ejemplo A1,X1 son public igual que A2 Y x2
// by default publicclassA1 {
var x1= 10
}
publicclassA2{
publicvar x2= 20
}
// specified with public modifierfun main() {
val a1 = A1()
println(a1.x1)
val a2 = A2()
println(a2.x2)
}
##Modificador private
Funciona de manera similar a como lo hace en Java. Cuando se utiliza private para modificar un miembro de una clase (un campo, método o clase anidada), ese miembro solo es accesible dentro de la misma clase. No se puede acceder desde fuera de la clase, ni siquiera desde las subclases.
classMyClass {
private val myPrivateField ="Soy un campo privado" fun myPublicFunction() {
println(myPrivateField) // Acceso permitido: dentro de la misma clase }
}
fun main() {
val myObject = MyClass()
myObject.myPublicFunction() // Imprime "Soy un campo privado"// println(myObject.myPrivateField) // Error: no se puede acceder a un miembro privado desde fuera de la clase}
##Modificador internal
Es un modificador de acceso específico de Kotlin que no existe en Java. El modificador internal se utiliza para restringir la visibilidad de un miembro de una clase a solo dentro del mismo módulo. Un módulo es un conjunto de archivos de código fuente de Kotlin compilados juntos. Por ejemplo, los ficheros de un proyecto IntelliJ se compilan juntos y por tanto son un módulo.
como organizar el código en módulos
Para organizar el código en módulos separados habrá que configurar el sistema de construcción apropiadamente (grandle, maven, …). Si estás utilizando el compilador de línea de comandos de Kotlin, cada invocación del compilador se trata como un módulo separado. Esto significa que si compilas varios archivos de código fuente juntos en una sola invocación del compilador, se tratarán como un solo módulo y los miembros marcados como internal serán accesibles entre ellos.
Supongamos que A1 está escrita en el fichero A1.kt
internal classA1 {
var x1= 10
}
y que A2 está escrita en el fichero A2.kt
classA2{
fun miFun(){
var x=A1()
}
}
Si ambas clases pertenecen al mismo módulo A2 no genera error de compilación ya que tiene acceso a A1. Si A2 perteneciera a otro módulo y quisiera acceder a A1, A1 tendría que ser public
¿Existe el modo acceso paquete de java en kotlin?
En Kotlin no hay un modificador de acceso específico para el nivel de paquete como en Java. En Java, si no se especifica un modificador de acceso para un miembro de una clase, ese miembro es accesible dentro del mismo paquete. Esto se conoce como acceso a nivel de paquete. En Kotlin, si no se especifica un modificador de acceso para un miembro de una clase, ese miembro es public por defecto y es accesible desde cualquier lugar.
Aunque Kotlin no tiene un modificador de acceso específico para el nivel de paquete, puedes lograr una funcionalidad similar utilizando el modificador internal y organizando tu código en módulos separados.
El modo protected
Cuando se utiliza protected para modificar un miembro de una clase (un campo, método o clase anidada), ese miembro solo es accesible dentro de la misma clase y sus subclases.
open classMyBaseClass {
protected val myProtectedField ="Soy un campo protegido" fun myPublicFunction() {
println(myProtectedField) // Acceso permitido: dentro de la misma clase }
}
classMyDerivedClass : MyBaseClass() {
fun myDerivedFunction() {
println(myProtectedField) // Acceso permitido: dentro de una subclase }
}
fun main() {
val myObject = MyDerivedClass()
myObject.myPublicFunction() // Imprime "Soy un campo protegido" myObject.myDerivedFunction() // Imprime "Soy un campo protegido"// println(myObject.myProtectedField) // Error: no se puede acceder a un miembro protegido desde fuera de la clase o sus subclases}
Es muy parecido a como funciona en Java pero no igual ya que en Java el modo protected también cubre los casos de modo de acceso paquete. Entonces el ejemplo anterior no es equivalente en Java ya que:
por defecto en java el mode de acceso es paquete pero en java public
no existe modo de acceso paquete en kotlin.
visibilidad de constructores
Si deseas especificar la visibilidad de un constructor en Kotlin, debes usar la palabra clave constructor y colocar el modificador de acceso antes de ella.
Si no especificas un modificador de acceso para el constructor, será public por defecto y puedes omitir la palabra clave constructor
classMyClass1publicconstructor(val myField: String) {
// ...}
classMyClass2constructor(val myField: String) {
// ...}
classMyClass3(val myField: String) {
// ...}
fun main() {
val a1 = MyClass1("a1")
println(a1.myField)
val a2 = MyClass2("a2")
println(a2.myField)
val a3 = MyClass3("a3")
println(a3.myField)
}
clases Enum
Vemos las posibilidades más esenciales de este tipo de clases. En la documentación oficial puedes consultar con más profundidad su uso.
Un enum en Kotlin es un tipo especial que representa un conjunto finito de valores predefinidos. Cada valor de un enum se representa como una instancia de una clase enum, que se define utilizando la palabra clave enum class.
enumclassDirection {
NORTH, SOUTH, EAST, WEST
}
fun main() {
val direction = Direction.NORTHwhen (direction) {
Direction.NORTH-> println("Going North")
Direction.SOUTH-> println("Going South")
Direction.EAST-> println("Going East")
Direction.WEST-> println("Going West")
}
}
Cada valor de un enum tiene propiedades predefinidas como name, que devuelve el nombre del valor como una cadena, y ordinal, que devuelve la posición del valor en la declaración del enum
enumclassColor(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
fun main() {
val color = Color.BLUEprintln(color.name) // Prints "BLUE" println(color.ordinal) // Prints "2" println(color.rgb) // Prints "255"}
Composición
Los dos grandes mecanismos de la POO que permiten reutilizar código son la composición y la herencia.
La composición en Kotlin es similar a la composición en Java. La composición es un principio de diseño en el que una clase tiene una relación “tiene-un” con otra clase. Esto se logra al tener una instancia de una clase como un campo en otra clase. en el siguiente ejemplo la clase Car tiene una propiedad de tipo Engine.
classEngine {
fun start() {
println("El motor está arrancando")
}
}
classCar(val engine: Engine) {
fun start() {
engine.start()
}
}
fun main() {
val engine = Engine()
val car = Car(engine)
car.start()
}
Herencia
¿Qué es herencia?
la herencia en programación orientada a objetos es uno de los mecanismos para compartir y reutilizar código.
El funcionamiento básico de este mecanismo consiste en que una clase hija puede heredar todos los miembros de una clase padre sin tener que volver a implementarlos. La clase hija puede agregar nuevos miembros o modificar los existentes, además de poder implementar su propia lógica adicional. Este relación de herencia entre dos clases tiene sentido en un contexto en el que el hijo es una especialización del padre, en caso contrario es preferible usar composición como mécanismo de reutilización de código.
Usaremos la siguiente terminología
Superclase o clase base: la clase que se hereda (el padre)
Subclase o clase derivada: la clase que recibe la herencia(el hijo)
un ejemplo sencillo
Supongamos que tenemos que trabajar con la clase persona y la clase Alumno. Observa como en nuestro caso el Alumno es una especialización de la clase Persona ya que un Alumno incluye todo el código de la clase Persona además del suuyo propio que en este sencillo ejemplo es la existencia de una propiedad adicional grupo
classPersona {
var nombre: String?=nullvar edad = 0
fun imprimirPersona() {
println("Datos personales: $nombre, $edad")
}
}
classAlumno {
var nombre: String?=nullvar edad = 0
var grupo = 0.toChar()
fun imprimirPersona() {
println("Datos personales: $nombre, $edad")
}
con el mecanismo de herencia, podemos indicar que la clase Alumno herede de la clase Persona y evitar todo el código duplicado del ejemplo anterior
open classPersona {
var nombre: String?=nullvar edad = 0
fun imprimirPersona() {
println("Datos personales: $nombre, $edad")
}
}
classAlumno : Persona() {
var grupo = 0.toChar()
}
fun main() {
val a1 = Alumno()
a1.nombre="Román" a1.edad= 14
a1.grupo='a' a1.imprimirPersona()
}
Sintaxis De Herencia En Kotlin
Lo más basico:
En la clase Base, palabra reservada open
En kotlin, por defecto, una clase no es heredable, hay que especificamente que queremos permitir que la clase sea heredable con la palabra reservada open
En la clase derivada los “:”
Para indicar cual es la superclase de una clase se añade “:” despues de su nombre y a continuación el nombre de la superclase, o mejor dicho, el constructor de la superclase.
Puedes comprobar estos detalles sintácticos en el ejemplo anterior.
Constructores y herencia
Cuando creamos un objeto derivado se crea uno base, por lo tanto el código de la clase derivada tiene que tener en cuenta que constructores tiene la clase base. Veamos algunas combinaciones.
clase base con constructor por defecto
Observa en el ejemplo que la clase base no tiene un constructor explícito definido y por tanto en la clase derivada se usa el constructor por defecto
open classBase{
var deBase="de base"}
classDerivada: Base() {
var deDerivada ="de derivada"}
fun main() {
val d = Derivada()
println(d.deBase)
println(d.deDerivada)
}
si la clase Derivada usa un constructor primario tendrá que referirse igualmente la constructor base por defecto
open classBase{
var deBase="de base"}
classDerivada(var deDerivada:String): Base()
fun main() {
val d = Derivada("de derivada")
println(d.deBase)
println(d.deDerivada)
}
si la clase base tiene un constructor primario, la derivada ya no puede usar el constructor por defecto y se ve obligada a usar dicho constructor primario
open classBase(var deBase:String)
classDerivada(var deDerivada:String): Base("de base")
fun main() {
val d = Derivada("de derivada")
println(d.deBase)
println(d.deDerivada)
}
puedes pobra a usar en el ejemplo anterior Base() y observar el error
Si Derivada no tiene primario y tiene un constructor secundario necesito usar la palabra reservada super para aludir al constructor de la clase base
open classBase(var deBase: String)
classDerivada : Base {
var deDerivada: String
constructor(deDerivada: String) : super("de base") {
this.deDerivada= deDerivada
}
}
fun main() {
val d = Derivada("de derivada")
println(d.deBase)
println(d.deDerivada)
}
en este último ejemplo, vuelvo a observar como desde un constructor secundario de derivada preciso usar super
open classBase(var deBase: String)
classDerivada : Base {
var deDerivada: String
constructor(deDerivada: String) : super("de base") {
this.deDerivada= deDerivada
}
constructor(deBase: String, deDerivada: String) : super(deBase) {
this.deDerivada= deDerivada
}
}
fun main() {
val d1 = Derivada("de derivada")
println(d1.deBase)
println(d1.deDerivada)
val d2 = Derivada("de base", "de derivada")
println(d2.deBase)
println(d2.deDerivada)
}
jerarquías de clases
En Kotlin, la herencia es simple, al igual que en Java. Esto significa que una clase solo puede heredar de una única clase base. No es posible heredar directamente de múltiples clases en Kotlin. Lo que sí es posible es que un hijo sea a su vez padre de otras clases.
Una clase B hereda de una A, pero una C puede heredar de B y otra D de C …. Es decir, la relación de herencia se puede extender de forma que podemos visualizar gráficamente como un arbol jeráquico.
open classAnimal(val nombre: String) {
fun hacerSonido() {
println("El animal hace un sonido.")
}
}
open classPerro(nombre: String) : Animal(nombre) {
fun ladrar() {
println("El perro ladra.")
}
}
classPastorAleman(nombre: String) : Perro(nombre) {
fun proteger() {
println("El Pastor Alemán protege su territorio.")
}
}
fun main() {
val pastorAleman = PastorAleman("Max")
println(pastorAleman.nombre)
pastorAleman.hacerSonido()
pastorAleman.ladrar()
pastorAleman.proteger()
}
La clase Any
En Kotlin, la clase Any es equivalente a la clase Object en Java en términos de herencia y funcionalidad básica.
La clase Any es la superclase de todas las clases en Kotlin. Todas las clases en Kotlin, de forma implícita o explícita, heredan de la clase Any
Igual que de Object java, de Any se heredan, entre otras, tres famosas propiedades toString(), hashCode() y equals()
classC1//hereda de Anyopen classC2 : Any() //hereda de AnyclassC3:C2() //hereda de C2 que hereda de Anyfun main() {
val c1 = C1()
val c2 = C2()
println(c1.toString())
println(c1.hashCode())
println(c1.equals(c2))
val c3=C3()
println(c3.toString())
}
Sobre escritura de funciones miembro
La sobrescritura de funciones en Kotlin permite que una clase derivada proporcione su propia implementación de una función definida en su clase base utilizando las palabras clave open y override.
open classSuperclase {
open fun funcionSobrescribible() {
println("Esta fimción puede ser sobrescrita")
}
}
classSubclase1 : Superclase() {
}
classSubclase2 : Superclase() {
override fun funcionSobrescribible() {
println("Esta es la nueva implementación de la función em Subclase2")
}
}
fun main() {
val subclase1 = Subclase1()
subclase1.funcionSobrescribible()
val subclase2 = Subclase2()
subclase2.funcionSobrescribible()
}
Observa que la función sobreescrita oculta a la función que se sobreescribe de la superclase. Si por la razón que sea, fuera necesario usar también la versión de la superclase desde la sublcase podemos hacerlo utilizando la palabra reservada super
open classSuperclase {
open fun funcionSobrescribible() {
println("Esta fimción puede ser sobrescrita")
}
}
classSubclase : Superclase() {
override fun funcionSobrescribible() {
super.funcionSobrescribible()
println("Esta es la nueva implementación de la función em Subclase2")
}
}
fun main() {
val subclase1 = Subclase()
subclase1.funcionSobrescribible()
}
Clases abstractas e Interfaces
Clases abstractas
Una clase abstracta es una clase que no se puede instanciar y por lo tanto está destinada a tener subclases que la extiendan. Una clase abstracta puede contener tanto métodos abstractos (métodos sin cuerpo) como métodos concretos (métodos con cuerpo).
Por lo tanto, Se utiliza una clase abstracta para proporcionar una interfaz común y una implementación para sus subclases. Cuando una subclase extiende una clase abstracta, debe proporcionar implementaciones para todos los métodos y propiedades abstractas definidos en la clase abstracta.
En Kotlin, una clase abstracta se declara usando la palabra reservada abstract delante de la clase. Una clase abstracta no puede instanciar, es decir, no podemos crear un objeto para la clase abstracta. También usamos la palabra reservada abstract para declarar propiedades y métodos abstractos. observa que una clase abstracta ya que su sentido es que tenga subclases, ya es por defecto open
//abstract classabstractclassEmployee(val name: String,val experience: Int) { // Non-Abstract// Property// Abstract Property (Must be overridden by Subclasses)abstractvar salary: Double
// Abstract Methods (Must be implemented by Subclasses)abstract fun dateOfBirth(date:String)
// Non-Abstract Method fun employeeDetails() {
println("Name of the employee: $name")
println("Experience in years: $experience")
println("Annual Salary: $salary")
}
}
// derived classclassEngineer(name: String,experience: Int) : Employee(name,experience) {
override var salary = 500000.00 override fun dateOfBirth(date:String){
println("Date of Birth is: $date")
}
}
fun main() {
//val emp = Employee("Praveen",2) ERROR no se puede instanciar una clase abstracta val eng = Engineer("Praveen",2)
eng.employeeDetails()
eng.dateOfBirth("02 December 1994")
}
interfaces
Las interfaces y las clases abstractas en Kotlin son similares. La diferencia principal es que las interfaces no pueden almacenar estado, mientras que las clases abstractas sí pueden. Las interfaces pueden tener propiedades, pero estas deben ser abstractas o proporcionar implementaciones de acceso.
interfaceMyInterface {
val prop: Int // propiedad abstracta fun foo() // función sin implementación fun bar() {
print(prop)
}
}
classMyClass : MyInterface {
override val prop = 29
override fun foo() {
print("foo")
}
}
fun main() {
val myClass = MyClass()
myClass.foo() // Imprime "foo" myClass.bar() // Imprime 29}
En Kotlin no es necesario utilizar la palabra clave abstract para declarar una propiedad o función abstracta en una interfaz.
Todas las funciones en una interfaz que no tienen un cuerpo son automáticamente abstractas y deben ser implementadas por las clases que implementan la interfaz. En el ejemplo anterior, la función foo en la interfaz MyInterface se declara sin un cuerpo, lo que significa que es abstracta.
En el siguiente ejemplo observamos que en una interfaz de Kotlin, una propiedad puede tener un valor si se proporciona una implementación para sus accesores (get y set).
interfaceMyInterface {
val prop: Int
get() = 29 // proporciona una implementación para el accesor get fun foo() {
print(prop)
}
}
classMyClass : MyInterface
fun main() {
val myClass = MyClass()
myClass.foo() // Imprime 29}
Pero entonces, ¿una interface puede tener estado?.
¡No! las interfaces en Kotlin no pueden tener estado. Aunque una propiedad en una interfaz puede tener una implementación para sus accesores (get y set), esta implementación no puede depender de un campo de respaldo (backing field) ya que las interfaces no pueden tener campos. Esto significa que el valor de una propiedad en una interfaz no puede cambiar, ya que no hay un campo para almacenar su estado.
Interfaces funcionales (SAM)
Concepto idéntico en Java pero con algunas diferencias sintácticas.
Una interfaz con un solo método abstracto se denomina interfaz funcional o interfaz de método abstracto único (SAM) . La interfaz funcional puede tener varios miembros no abstractos pero solo un miembro abstracto.
Para declarar una interfaz funcional en Kotlin se usa el modificador fun.
fun interfaceMyFunctionalInterface {
fun myFunction(s: String)
}
fun main() {
val myObject = MyFunctionalInterface { s -> println(s) }
myObject.myFunction("Hola mundo")
}
los usaremos cuando estudiemos programación funcional.
Ejemplo de polimorfismo con clase abstracta e interface
El polimorfismo es uno de los cuatro principios fundamentales de la programación orientada a objetos (junto con la encapsulación, la herencia y el abstracción). El polimorfismo permite que objetos de diferentes clases se traten como objetos de una clase común. Esto se logra mediante el uso de clases abstractas o interfaces. Cuando utilizar un interface o una clase abstracte es una cuestión de diseño orientado a objetos que trataremos más adelante. En el siguiente ejemplo, tal y como se plantea, conseguimos con ambos el mismo efecto.
Ejemplo con clase abstracta:
abstractclassShape {
abstract fun draw()
}
classCircle : Shape() {
override fun draw() {
println("Dibujando un círculo")
}
}
classRectangle : Shape() {
override fun draw() {
println("Dibujando un rectángulo")
}
}
fun main() {
val shapes = listOf(Circle(), Rectangle())
for (shape in shapes) {
shape.draw()
}
}
Ejemplo con interfaz:
interfaceShape {
fun draw()
}
classCircle : Shape {
override fun draw() {
println("Dibujando un círculo")
}
}
classRectangle : Shape {
override fun draw() {
println("Dibujando un rectángulo")
}
}
fun main() {
val shapes = listOf(Circle(), Rectangle())
for (shape in shapes) {
shape.draw()
}
}
El polimorfismo es útil porque permite escribir código más genérico y reutilizable. En lugar de escribir código específico para cada clase, puedes escribir código que funcione con objetos de una clase abstracta o interfaz común y dejar que el polimorfismo se encargue de llamar a la implementación correcta del método.
Data Clases
A menudo creamos clases para contener algunos datos. En tales clases, algunas funciones estándar a menudo se derivan de los datos. En Kotlin, este tipo de clase se conoce como data class y se marca como data .
data classUser(val name: String, val age: Int)
El compilador genera automáticamente en base a los datos las siguientes funciones de todas las propiedades declaradas en el constructor principal:
equals()/ hashCode()
toString()de la forma"User(name=John, age=42)"
componentN()
copy()
Las funciones equals() hashCode() y toString() tienen el mismo objetivo que en Java y ya las conocemos. La función copy permite crear una copia de un objeto con algunos campos modificados. Y las funciones componentN() tienen que ver con el concepto kotlin de desestructuración que veremos más adelante.
data classUser(val name: String, val age: Int)
fun main() {
val user1 = User("Alice", 25)
val user2 = User("Bob", 30)
val user3 = User("Alice", 25)
println(user1) // Imprime "User(name=Alice, age=25)" println(user1 == user2) // Imprime "false" println(user1 == user3) // Imprime "true" val user4 = user1.copy(age = 35)
println(user4) // Imprime "User(name=Alice, age=35)"}
La palabra reservada object. Companion object, objetos singleton y objetos expressions
La palabra clave “object” en Kotlin no tiene el mismo significado que la palabra clave “object” en Java. Aunque comparten el mismo nombre, en Kotlin “object” se utiliza para diferentes propósitos. La palabra reservada “object” java se corresponde con “any” en Kotlin como ya vimos al estudiar herencia.
En Kotlin, la palabra clave “object” se utiliza para:
declarar miembros estáticos
declarar objetos singleton
declarar objetos anónimos con object expressions
declarar miembros estáticos
Un miembro estático en Java es un miembro de una clase que se puede acceder sin tener que crear una instancia de esa clase. Se declaran con la palabra clave static y se acceden a través del nombre de la clase. Los datos estáticos no se almacenan dentro de cada objeto si no en una zona de memoria estática común a todos los objetos.
En Kotlin, no existe una palabra clave static como en Java para declarar miembros estáticos en una clase. En su lugar, Kotlin utiliza el modificador companion object para crear miembros estáticos.
El companion object es un objeto que se asocia con la clase y permite definir propiedades y funciones que se pueden acceder directamente a través del nombre de la clase, sin necesidad de crear una instancia de la clase.
classMiClase {
companion object {
val miVariable: Int = 10
fun miFuncion() {
println("Soy una función estática.")
}
}
}
fun main() {
println(MiClase.miVariable)
MiClase.miFuncion()
}
El companion object en Kotlin ofrece algunas ventajas en comparación con el modificador static en Java. Algunas de estas ventajas precisan de conceptos que aun no vimos para analizarlas, pero por el momento podemos quedarnos con una sencilla que es que mejora la legibilidad del código.
Kotlin, al igual que Java, utiliza una zona de memoria estática para almacenar información que es compartida por todas las instancias de una clase. Esta zona de memoria se utiliza para almacenar miembros estáticos en Java o miembros de un objeto compañero en Kotlin.
En Kotlin, cuando se declara un objeto compañero dentro de una clase, sus miembros se almacenan en la zona de memoria estática y se pueden acceder directamente desde la clase sin necesidad de crear una instancia de esa clase. Esto permite que los miembros del objeto compañero se compartan entre todas las instancias de la clase.
La gestión de la memoria estática en Kotlin es manejada por la máquina virtual de Java (JVM), ya que Kotlin es un lenguaje que se ejecuta en la JVM. La JVM es responsable de asignar y liberar la memoria estática según sea necesario.
declarar objetos singleton
A diferencia de Java, donde implementarías el patrón Singleton manualmente, en Kotlin puedes declarar un objeto como un singleton directamente utilizando la palabra clave “object”. Por ejemplo:
object MiSingleton {
fun hacerAlgo() {
println("Haciendo algo en el singleton")
}
}
fun main() {
MiSingleton.hacerAlgo()
//val miSingleton = MiSingleton() //ERROR no se puede crear una instancia de un singleton}
declarar objetos anónimos con object expressions
Las expresiones de objeto (object expressions) en Kotlin crean objetos de clases anónimas, es decir, clases que no están explícitamente declaradas con la declaración de clase. Estas clases son útiles para un solo uso. Puedes definirlas desde cero, heredar de clases existentes o implementar interfaces.
Ejemplo desde cero
Con “desde cero” nos referimos a que no se implica la herencia ni la implementación de interfaces.
fun main() {
val helloWorld = object {
val hello ="Hello" val world ="World" override fun toString() ="$hello $world" }
println(helloWorld)
}
El ejemplo anterior puede ser útil si necesito simplemente una instancia y no me importa en absoluto el nombre de la clase ya que tengo el objeto asociado a una variable y sólo lo usaré a través de dicha variable. Sin object expression el código equivalente sería
classHelloWorld {
val hello ="Hello" val world ="World" override fun toString() ="$hello $world"}
fun main() {
val helloWorld = HelloWorld()
println(helloWorld)
}
Ejemplo heredando de un super tipo
El ejemplo anterior podríamos llamarlo “heredando desde any”. Si queremos crear un objeto de una clase anónima pero que a su vez esa clase hereda de otra clase distinta de any usamos la sintaxis de herencia
open classA(x: Int) {
public open val y: Int = x
}
fun main() {
val a = object : A(1){
override val y = 15
}
println(a.y)
}
Similar para implementar un interface
interfaceB {
fun saludo()
}
fun main() {
val b1 = object : B {
override fun saludo() {
println("Hola desde b1")
}
}
b1.saludo()
}
También se podría heredar de una clase e implementar simultáneamente muchos interfaces como con las clases con nombre.
Excepciones en Kotlin
Prácticamente es idéntico a java pero hay una importante diferencia: En Kotlin, solo hay excepciones no verificadas(unchecked) que se lanzan durante la ejecución del programa en tiempo de ejecución.
Las excepciones verificadas (checked) (marcadas) se introdujeron en java como una característica para mejorar la calidad del códgio pero con el paso del tiempo la experiencia final de los programadores es que en realidad disminuye la productividad sin ningún aumento adicional en la calidad del código. Entre otros problemas, las excepciones verificadas producen código repetitivo y muy importante hoy en día, se hacen dificiles de usar conjuntamente con expresiones lambda.
Por esto, como en muchos otros lenguajes de programación modernos, los desarrolladores de Kotlin también decidieron no incluir excepciones verificadas como una característica del lenguaje.
A continuación vemos dos características “menores” de las excepciones Kotlin que no hay en java
try/catch es una expresión y devuelve un valor.
Como ocurría con el if, cuando nos interese podemos aprovechar el hecho de que try/catch devuelve un valor
fun divide(x: Int, y: Int): Int {
val result =try {
x / y
} catch (e: ArithmeticException) {
0
}
return result
}
fun main() {
println(divide(10, 2))
println(divide(10, 0))
}
Nothing es un tipo especial en Kotlin que se utiliza para representar un valor que nunca existe.
Se puede usar Nothing como el tipo de retorno de una función que nunca termina, como una función que entra en un bucle infinito:
fun infiniteLoop(): Nothing {
while (true) {
// ... }
}
Para indicar el tipo de un elemento en una lista vacía para indicar que la lista no contiene elementos:
val emptyList = listOf<Nothing>()
y lo que nos interesa ahora, si una función devuelve Nothing, es una función cuyo retorno jamás se puede alcanzar lo que es equivalente a que siempre lanza una excepción.
fun failWithException(): Nothing {
throw Exception("Error occurred")
}
Puedes consultar la documentación oficial de excepciones. Observa que la sintáxis es igual a java salvo alguna añadido como que try es una expresión y podemos usar el valor que devuelve como ocurria con el if.
https://kotlinlang.org/docs/exceptions.html
Genéricos
clases genéricas, parámetro de tipo y argumento de tipo
Aquí la palabra “parámetro” y “argumento” la vamos a aplicar a los tipos de las clases. No confundir con los parámetros y argumentos de una función (aunque es puedan establecer paralelismos).
Se supone que conoces los rudimentos de genéricos en Java.
Las clases en Kotlin se pueden declarar usando parámetros de tipo, al igual que en Java. El parámetro de tipo consite en una letra mayúscula como T encerrada entre <>. De T decimos que es un tipo genérico y una clase que usa un tipo genérico es una clase genérica. Una clase genérica puede tener más de un tipo genérico, es decir, usar más de una letra
classBox<T>(t: T) {
var value = t
}
¡Las clases pueden ser genéricas pero los objetos no!
Cuando instanciamos una clase genérica se proporciona un argumento para el parámetro. El argumento será un tipo concreto.
val intBox:Box<Int>= Box<Int>(1)
val stringBox:Box<String>= Box<String>("Hello")
Pero si los parámetros se pueden deducir, por ejemplo, de los argumentos del constructor, puede omitir los argumentos de tipo, por ejemplo
val box = Box(1) // 1 has type Int, so the compiler figures out that it is Box<Int>
los wildcardas de java ? *
Al trabajar con genéricos los caracteres ? y * que se combinan con extends y super no se usan en kotlin. Se sustiuyeron por un mecanimos denominado varianza
Varianza.
Vemos sólo la idea genérica sin profundizar. Es un tema importante para los programadores que necesitan escribir clases genéricas de cierto nivel de complejidad.
La varianza se refiere a cómo los subtipos de un tipo genérico se relacionan con los subtipos de sus parámetros de tipo. Hay tres tipos de varianza: covarianza, contravarianza e invarianza.
Covarianza: Si A es un subtipo de B, entonces Box<A> es un subtipo de Box<B>. Esto se logra utilizando el modificador out en el parámetro de tipo de Box.
Contravarianza: Si A es un subtipo de B, entonces Box<B> es un subtipo de Box<A>. Esto se logra utilizando el modificador in en el parámetro de tipo de Box.
Invarianza: Ninguna relación entre los subtipos. Esto significa que aunque A sea un subtipo de B, no hay ninguna relación entre Box<A> y Box<B>. Esto es lo que sucede cuando no se utiliza ningún modificador en el parámetro de tipo.
Un typealias en Kotlin es una forma de proporcionar un nombre alternativo para un tipo existente. Al usar genéricos y y en otras situaciones de manejo de tipos, se puede utilizar esta claúsula para abreviar nombres de tipos largos o para proporcionar nombres más descriptivos para tipos que pueden ser confusos.
Un typealias se declara utilizando la palabra clave typealias, seguida del nuevo nombre para el tipo y el tipo existente al que se refiere. En el siguiente ejemplo se ilustra el funcionamiento sintáctico de typealiases.
typealias StringList = List<String>fun printAll(strings: StringList) {
for (string in strings) {
println(string)
}
}
fun main() {
val names: StringList = listOf("Alice", "Bob", "Charlie")
printAll(names)
}
Delegación con cláusula by
Para entender bien el objeto de la cláusula by es necesario entender el patrón Delegation y su relación con la herencia multiple a su vez relacionado con el importante tema tan discutido “herencia vs composición”. Todas estas cuestiones se tratan en boletines a parte.
La cláusula by
En el siguiente ejemplo una clase Derived puede implementar una interfaz Base delegando todos sus miembros públicos a un objeto específico. La cláusula by provocara que se almacenará internamente en un objetos de Deriveda un objeto b y se generarán todos los métodos de Base necesarios para reenvío a b.
interfaceBase {
fun print()
}
classBaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
classDerived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
El código equivalente sin cláusula by sería el siguiente
interfaceBase {
fun print()
}
classBaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
classDerived(b: Base) : Base {
private val base: Base = b
override fun print() {
base.print()
}
}
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
Clases anidadas e internas
Clases anidadas
Una clase puede estar anidada (nested) dentro de otra. La clase anidada pasa a ser un miembro de dicha clase. Por lo tanto los miembros de una clase pueden ser propiedades, funciones y ¡otras clases!.
classOuter {
private val bar: Int = 1
classNested {
fun foo() = 2
}
}
fun main() {
val demo = Outer.Nested().foo() // == 2 println(demo)
}
en kotlin hay más combinaciones de posibilidades de anidamiento que en java ya que se pueden anidar interfaces en clases y viceversa
no es más que una clase anidada marcada con inner . Esta marca permite que la clase anidada acceda a los miembros de la clase exterior. En java el acceso a la clase exterior era inmediato, en kotlin necesitamos especificar este deseado con inner.
En el siguiente ejemplo hay error de compilación ya que foo() no puede acceder a bar
classOuter {
val bar: Int = 1
classInner {
fun foo() = bar
}
}
fun main() {
val demo = Outer().Inner().foo() // == 1 println(demo)
}
si añadimos inner a la clase Inner podemos acceder a bar, incluso aunque bar sea private
classOuter {
private val bar: Int = 1
inner classInner {
fun foo() = bar
}
}
fun main() {
val demo = Outer().Inner().foo() // == 1 println(demo)
}
También se les llama simplemente anónimas. Son las más usadas e importantes de las clases anidadas.
Se declaran usando la sintáxis de object expressions que ya vimos con anterioridad.
En el siguiente ejemplo anidamos una object expressión dentro de la clase MyActivity, pero recuerda que se pueden usar las object expresión en cualquier parte, por ejemplo directamente asociadas a una variable de la función main.
interfaceGreeter {
fun greet(name: String)
}
classPerson(val name: String) {
var greeter: Greeter?=null fun greet() {
greeter?.greet(name)
}
}
classMyActivity {
val person = Person("Alice")
init {
person.greeter= object : Greeter {
override fun greet(name: String) {
println("Hello, $name!")
}
}
}
}
fun main() {
val activity = MyActivity()
activity.person.greet()
}
Programación funcional
Breve introducción a la programación funcional utilizando el lenguaje Kotlin.
Subsecciones de Programación funcional
sintáxis avanzada de las funciones kotlin
PROGRAMACION FUNCIONAL 1: MÁS DE SINTAXIS DE FUNCIONES
La programación funcional se basa esencialmente en el uso de funciones
por lo que es conveniente observar sintaxis adicionales relativas a
funciones que no vimos cuando estudiamos el concepto de función en la
introducción a la programación estructurada.
Argumentos con nombre
al invocar a una función podemos indicar el nombre del parámetro al que
queremos asociar el argumento. Esto es útil cuando la función tiene
muchos argumentos para tener clara la correspondencia. Está técnica
incluso nos permite cambiar el orden or defecto de los argumentos.
fun sumar(a: Int, b: Int): Int {
return a + b
}
val resultado1 = sumar(2, 3)
print(resultado1)
val resultado2 = sumar(b = 3, a = 2) // usando nombres puedo cambiar ordenprint(resultado2)
55
Numero variable de argumentos con varargs
Si marcamos un parámetro con varargs quiere decir que dicho parámetro
puede recibir un número variable de argumentos
fun suma(vararg numeros: Int): Int {
var resultado = 0
for (numero in numeros) {
resultado += numero
}
return resultado
}
println(suma(1, 2, 3, 4))
println(suma(5, 10, 15))
10
30
Realmente lo que se hace internamente es generar un array, en el caso
anterior un array de enteros. ¿Cual es entonces la diferencia con
declarar un array como parámetro?, pues que con esta sintáxis el
argumento puede ser un array pero también una lista de los valores
separados por comas con los que automáticamente se genera un array.
Funciones genéricas
Se puede escribir una función utilizando un tipo o varios genéricos.
Este tipo genérico se puede usar para escribir el resto de la función
tanto en los parámetros como en el cuerpo.
Los tipos genéricos se indica despues de la palabra fun indicando una
letra para representar el tipo genérico entre <>
fun <T>imprimirValor(valor: T) {
println("El valor es: $valor")
}
//observa como puedo invocar a la función con argumentos de distinto tipoimprimirValor(42)
imprimirValor("hola")
imprimirValor(true)
El valor es: 42
El valor es: hola
El valor es: true
Funciones infix
Una función infix es una función que se puede llamar utilizando la
sintaxis de operador infijo en lugar de la sintaxis típica de llamada de
función. Una función infix sólo puede tener un parámetro
classPersona(val nombre: String) {
infix fun esTocayoDe(persona:Persona): Boolean {
returnthis.nombre== persona.nombre }
}
val x = Persona("Juan")
val y = Persona("Pedro")
println(x.esTocayoDe(y))
println(x esTocayoDe y) //uso innix
false
false
Alcance de las funciones
Igual que en un lenguaje tradicional hablamos del alcance “scope” de una
variable, también las funciones tienen un alcance.
Si clasificamos las funciones por alcance tenemos los siguientes tipos
de funciones.
funciones top-level. Si lo deseamos en kotlin podemos declarar una
función el nivel superior de archivo, lo que significa que no se
necesita crear una clase para contener una función como en java o c#
funciones locales. Una función puede definirse dentro de una función
y pasa a ser una función interna que llamamos local
funciones miembro. El alcance de la función está ligado a un objeto,
similar a un método java.
funciones de extensión. El alcance es similar a las funciones
miembro. Estuvimos utilizando constantemente funciones top-level y
también escribimos funciones miembreo al ver contenidos de
programación orientada objetos en Kotlin. Veremos a continuación
ejemplos de funciones locales y de extensión.
Funciones locales
Una función local f es una función que se define dentro de otra
función **g **. La funcion local f solo puede ser accedida por las
instrucciones de g, no fuera de g. Son útiles cuando al escribir
una función observamos que duplicamos código o simplemente para tener el
código un poco más organizado.
el siguiente ejemplo es inútil pero ejemplificador de la sintaxis. La
función sumar() es una función local ya que está definida dentro de otra
llamada calcularSuma(). Desde fuera de calcularSuma la función sumar()
no es accesible ya que es algo “local” a calcularSuma().
fun calcularSuma(a: Int, b: Int): Int {
fun sumar(x: Int, y: Int): Int {
return x + y
}
val resultado = sumar(a, b)
return resultado
}
print(calcularSuma(3,4))
//print(sumar(3,2))
7
Extensión de funciones
Las extensiones de función permiten extender la funcionalidad de clases
existentes añadiendo funciones pero sin modificar su código fuente
original añadiendo funciones. Esto puede ser especialmente útil cuando
se trabaja con bibliotecas y frameworks que no podemos o queremos
modificar.
En el siguiente ejemplo añadimos a la clase standar Int la función
duplicar(), observa que podemos ver la extensión de funciones como una
suerte de “extend express” a la clase Int
fun Int.duplicar(): Int {
returnthis*2
}
val x = 5
print(x.duplicar())
10
En el siguiente ejemplo creamos una función de extensión para Int que
además puede llamarse en modo infix
infix fun Int.sumAndMultiply(other: Int): Int {
return (this+ other) * other
}
print(3 sumAndMultiply 4) //(3+4)*4
28
Sobrecarga de operadores
La sobrecarga de operadores en Kotlin se refiere a la capacidad de
definir cómo se comportará un operador determinado en una clase
personalizada. En otras palabras, permite que los operadores estándar,
como +, -, *, /, etc., se utilicen con objetos de una clase
personalizada.
Por ejemplo, queremos sumar objetos personas, para ello en la clase
Persona debemos sobreescribir la función plus(). La función plus() está
asociada por definición del lenguaje al operador + de tal forma que el
código que escribamos dentro de la función plus de la clase Persona, es
el código que se va a ejecutar si relacionamos dos personas con el
operador +. Puedes consultar en kotlin.org todos los operadores que se
pueden sobrecargar y sus funciones asociadas.
classPersona(val nombre: String, val edad: Int) {
operator fun plus(other: Persona): Persona {
val nuevoNombre ="${this.nombre} y ${other.nombre}" val nuevaEdad =this.edad+ other.edadreturnPersona(nuevoNombre, nuevaEdad)
}
}
val persona1 = Persona("Juan", 30)
val persona2 = Persona("María", 25)
val persona3 = persona1 + persona2
println(persona3.nombre) // Juan y Maríaprintln(persona3.edad) // 55
Juan y María
55
//ahora la suma de dos personas devuelve un entero que corresponde a la suma de edades de dos personasclassPersona(val nombre: String, val edad: Int) {
operator fun plus(other: Persona): Int {
returnthis.edad+ other.edad }
}
val persona1 = Persona("Juan", 30)
val persona2 = Persona("María", 25)
val edad = persona1 + persona2
println(edad)
55
Funciones y eficiencia
Debido al uso intensivo de funciones en kotlin los modificadores inline
y tailrec se utilizan para mejorar el rendimiento en determinados
escenarios.
Funciones inline
Una función inline en Kotlin es una función que, en tiempo de
compilación, se “copia y pega” el cuerpo de la función en cada lugar
donde se la llama en lugar de generar una llamada a la función. Esto
puede aumentar el rendimiento del programa al evitar el costo de la
creación y el desmantelamiento de la pila de llamadas en tiempo de
ejecución. Pero este efecto no es visible en el código fuente.
En el código fuente simplemente se añade el modificador inline y
luego ya se encarga el compilador de hacer esa especie de “pegado de
código” para eliminar las llamadas a la pila
En general, se recomienda utilizar la anotación inline solamente en
aquellas funciones que son llamadas con frecuencia y que contienen una
cantidad significativa de código. Si declaramos indiscriminadamente
inline todas las funciones obtendríamos un código compilado más grande y
se reduciría el rendimiento general del programa.
Nuestro objetivo ahora no es saber cuando compensa o no compensa
declarar una función* inline*, nos basta saber como trabaja inline de
forma que cuando usemos funciones de la biblioteca standard y
consultemos su documentación, si éstas tienen el modificador* inline*
sepamos simplemente que se refiere a una cuestión de eficiencia, es
decir, que no tiene que ver con los resultados que devuelve la función y
por lo tanto que podemos en principio despreocupar que sea inline o
no.
inline fun saludo(nombre: String): String {
return"Hola, $nombre!"}
val miNombre ="Juan"val mensaje = saludo(miNombre)
println(mensaje) // imprime "Hola, Juan!"
Hola, Juan!
funciones tail recursive
Ya sabemos que las funciones se pueden llamar recursivamente, por
ejemplo
fun factorial(n: Int): Int {
returnif (n == 1 || n == 0) {
1
} else {
n * factorial(n - 1)
}
}
print(factorial(4))
24
La recursividad es una de las herramientas de la programación funcional.
La recursividad es muy expresiva y permite resolver cierto tipo de
problemas muy complicados con facilidad. No obstante, es mucho menos
eficiente que la iteración.
Kotlin permite declarar las funciones recursivas como tail recursive y
esto hace que automáticamente en el código compilado se genere de una
versión iterativa eficiente de nuestra versión recursiva. Es decir, esta
técnica nos permite disfrutar de la expresividad de la recursividad y de
la eficiencia de la iteración.
Para que una una función sea considerada como *“tail recursive” *tiene
que cumplir:
la llamada recursiva es la última operación que se ejecuta en la
función.
se especifica el modificador tailrec lo que avisa al compilador que
haga la traducción a versión iterativa correspondiente.
El ejemplo anterior de factorial, no ese puede declarar como tailrec ya
que la ultima instrucción return hay una operación aritmética, no una
simple llamada recursiva. La siguiente versión de factorialsí tailrec
tailrec fun factorial(n: Int, acc: Int = 1): Int {
returnif (n == 0) {
acc
} else {
factorial(n - 1, acc * n)
}
}
print(factorial(4))
24
Se pierde libertad y expresividad al escribir el código, pero de esta
manera se pueden evitar el bajo rendimiento y los desbordamientos de
pila ya que el compilador trabaja internamente con version iterativa que
el propio compilador genera automáticamente.
Muchos programadores que necesitan resolver un problema típicamente
recursivio siguen este proceso:
Escribe su código recursivo libremente disfrutando de la
expresividad de la recursividad.
Una vez alcanzada la solución y bien entendido el problema, si se
preveen desbordamientos de pila o problemas de eficiencia se intenta
reescribir la solución anterior como tail recursive para que el
compilador genere en el código compilado al versión iterativa. No
siempre es posible obtener una versión tail recursive, depende del
problema a resolver.
Si nuestra solución no tiene una versión equivalente escrita en
formato tail recursive, entonces el programador intenta hacer él
mismo una versión iterativa para lo que existen diversas técnicas
algorítmicas.
Asignar funciones a variables. El operador de referencia a funciones ::
Se puede utilizar el operador “: :” para asignar una función a una
variable o a una constante. Este operador se conoce como operador de
referencia de función y permite crear una referencia a una función
existente. Es posible asignar una función a una variable de varias
formas, por ejemplo con el operador ::
fun sumar(a: Int, b: Int): Int {
return a + b
}
fun restar(a: Int, b: Int) : Int {
return a - b
}
var miFuncion = ::sumar
println(miFuncion(2,3))
miFuncion = ::restar
println(miFuncion(10,15))
5
-5
Si la función es miembro de una clase también se puede obtener una
referencia a dicha función con la sintaxis Clase::funcion
classPersona(val nombre: String) {
fun saludar() {
println("Hola, soy $nombre")
}
}
val funMiembroReferencia = Persona::saludar
val persona = Persona("Juan")
funMiembroReferencia(persona) //equivalente a persona.saludar()
Hola, soy Juan
Sintaxis para expresar tipos de función. Firma de la función
En ciertas situaciones necesitamos referirnos al tipo de una función. El
tipo de una función se refiere al conjunto formado por del tipo de sus
parámetros y el tipo de retorno. A este conjunto también se le conoce
por el término firma de la función. La sintaxis general es:
(lista de los tipos de parámetros separados por comas) -> tipo de
retorno
Ejemplos
(Int, Int) -> Int describe una función que se le pasan dos enteros y
devuelve un entero
(Int) -> String función que se le pasa un entero y devuelve un String
() -> String función que no se le pasa nada y devuelve un String
(Int) -> Unit función que se le pasa un Int y no devuelve nada
Funciones anónimas
Una función anónima es una función que no tiene un nombre explícito
asociado a ella.
Las funciones anónimas son útiles cuando necesitamos definir una función
simple que se utilizará solo una vez en el programa y no es necesario
darle un nombre explícito. También es interesante cuando queremos
asociar una función a una variable.
val sum = fun(a: Int, b: Int): Int {
return a + b
}
// Llamada a la función anónimaval result = sum(2, 3)
println("El resultado de la suma es $result")
El resultado de la suma es 5
Funciones lambda
Las funciones lambda son otra forma de escribir funciones anónimas. La
diferencia principal entre una función anónima y una lambda es que la
sintaxis para definir una función lambda es más corta y concisa. Observa
que la sintaxis de una función lambda usa la sintaxis tipo de función
que comentamos más arriba. De hecho, se puede ver a una expresión lambda
como una instancia de un tipo de función.
En Kotlin, una función lambda se define utilizando la sintaxis {
argumentos -> cuerpo de la función }. La función lambda también puede
ser asignada a una variable o pasada como un parámetro a otra función,
de manera similar a una función anónima.
En general se usan más la lambda que las funciones anónimas, no
obstante, en ciertas situaciones que iran apareciendo poco a poco se
podra observar que hay situaciones en las que se prefieren las funciones
anónimas
val sum = { a: Int, b: Int -> a + b } //ahora como lambda// Llamada a la función lambdaval result = sum(2, 3)
println("El resultado de la suma es $result")
El resultado de la suma es 5
Relación entre lambda y tipo de función
La firma de la lambda { a: Int, b: Int -> a + b } en Kotlin es (Int,
Int) -> Int
Todas las funciones tienen una firma y por tanto se describe su firma
con un tipo de función. Por ejemplo
fun suma(a: Int, b: Int): Int { return a + b }
su firma también es (Int, Int) -> Int
Por lo tanto los tipos de función describen la firma de una función.
Ocurre además que la descripción de la firma se hace con una sintáxis
bastante parecida, aunque no igual, a la sintaxis lambda, pero son cosas
diferentes. Con una lambda creamos una instancia de una función. La
sintáxis de tipos de función aquí la emplemaos “teóricamente” para
también se usa en el código en ciertas situaciones, por ejemplo, para
describir el tipo de un parámetro que queremos que reciba como argumento
una función. Esto lo veremos más adelante.
Funciones que aceptan otra función como parámetro
Esta es una de la situaciones que la necesitamos expresamente incluir en
nuestro código la sintaxis de tipos de función.
Un parámetro no es más que un tipo especial de variable local. Como para
toda variable debo indicar su tipo al definirla. Si quiero que la
variable se asocie a un Int indicaré el tipo Int, pero si quiero que la
variable se asocie a una función, indicare ¡el tipo de la función!
fun sumar(a: Int, b: Int): Int {
return a + b
}
fun restar(a: Int, b: Int): Int {
return a - b
}
fun operarDosNumeros(num1: Int, num2: Int, operacion: (Int, Int) -> Int): Int {
return operacion(num1, num2)
}
println(operarDosNumeros(5, 3, ::sumar))
println(operarDosNumeros(5, 3, ::restar))
8
2
Sintaxis especial para invocar funciones que tienen por último parámetro una lambda
Es una sintaxis muy utilizada y merece una indicación especial.
Cuando se llama a una función, que a su vez tiene una función lambda
como último argumento, se pueden escribir este último argumento fuera de
los paréntesis. Es por una cuestión de legibilidad y se recomienda este
formato en la guia de estilo kotlin.
fun restar(a: Int, b: Int): Int {
return a - b
}
fun operarDosNumeros(num1: Int, num2: Int, operacion: (Int, Int) -> Int): Int {
return operacion(num1, num2)
}
println(operarDosNumeros(5, 3, ::restar))
//ahora vamos a sumar con lambdaprintln(operarDosNumeros(5, 3, {x,y->x+y}))
println(operarDosNumeros(5, 3) {x,y->x+y}) //se prefiere
2
8
8
Funciones que devuelven una función
Otra situación en la que necesito la sintáxis de tipo de función.
Como sabemos cuando escribimos la definición de una funcion, tiene que
concordar el tipo indicado en la cabacera con el tipo de lo que se
devuelve en el return, por lo tanto:
en la definición de la función, en la cabecera, al indicar el tipo
de retorno de la función observaremos que como queremos devolver una
función el tipo de retorno se describe con un tipo de función. En el
ejemplo de abajo con (Int))->Int
en el return devolveremos una función. En el ejemplo conseguimos
devolver una función a través de una lambda que concuerda con el
tipo de retorno indicado en la definición de la función.
Veamos dos ejemplos, que efectivamente no son muy útiles pero
ejemplifican de forma sencilla el concepto de “devolver una función”
fun square(x: Int) = x * x
fun operation(): (Int) -> Int {
return ::square
}
val func = operation()//operation() devolvió la función square() que ahora está enganchada a la variable funcprintln( func(4) )
16
fun sumar(num1: Int): (Int) -> Int {
return { num2 -> num1 + num2 }
}
val sumarCinco = sumar(5) //podemos imagina que sumarCinco tiene dentro la lambda {num2->5+num2}println(sumarCinco(3))
8
La keyword it en expresiones lambda
Cuando la expresión lambda sólo tiene un parámetro la expresión lambda
se puede simplificar asumiendo que dicho parámetro se llama it, lo que
nos permite escribir todo más conciso
val incrementarEn1: (Int) -> Int = { x -> x + 1 }
//val incrementarEn1 = { x:Int -> x + 1 } así tb se infieren tiposval y = incrementarEn1(5)
println(y)
//ahora con itval incEn1: (Int) -> Int = { it + 1 }
val x = incrementarEn1(5)
println(x)
6
6
algunos conceptos básicos de programación funcional
(algunas) caracteristicas básicas del paradigma de programación funcional
Es un tema extenso. Las siguiente lista de caracterísiticas no es una
lista definitiva y son simplemente un punto de partida para ir
comprendiendo que es la programación funcional.
Las funciones son ciudadanos de primera clase: Las funciones son
valores que pueden ser asignados a variables, pasados como
argumentos y devueltos como resultados.
Inmutabilidad de los datos: Los datos no deben cambiar una vez que
se han creado. En lugar de modificar los datos existentes, las
funciones deben crear nuevas estructuras de datos a partir de las
existentes.
Programación basada en expresiones: Las expresiones son evaluadas
para producir valores. Las expresiones son preferibles a las
sentencias, que modifican el estado.
Evaluación perezosa: Los valores se calculan solo cuando se
necesitan. Esto puede mejorar la eficiencia de los programas al
evitar el cálculo innecesario de valores que no se utilizan.
Declaratividad: Los programas se definen en términos de qué se debe
hacer, no de cómo hacerlo.
Características más importantes que deben cumplir las funciones para trabajar con el paradigma de programación funcional
Para atender a los principios de este paradigma las funciones del
lenguaje deben de:
ser funciones puras
permitir la composición de funciones
permitir el trabajo con recursividad
poder comportarse como funciones de alto orden
Funciones puras
Las funciones deben producir el mismo resultado para una entrada dada y
no tener efectos secundarios. Esto hace que las funciones sean más
fáciles de razonar y depurar. La siguiente funcion es pura, en el
ejemplo siempre que recibe los valores 3 y 4 produce 7
fun suma(num1:Int,num2:Int) = num1+num2
println(suma(3,4))
println(suma(3,4))
println(suma(3,4))
println(suma(3,4))
7
7
7
7
esta característica es muy importante para evitar los temidos efectos
colaterales del uso de variables de alcance no local a la función.
La siguiente función no es pura ya que si la invocamos con los mismos
argumentos puede producir diferentes valores.
var total=0
fun suma(num1:Int,num2:Int):Int{
total=total+num1+num2
return total
}
println(suma(3,3))
println(suma(3,3))
6
12
Dicho de forma más llana, las funciones son puras si trabajan siempre
con variables locales. Utilizar variables NO locales genera aplicaciones
más dificiles de depurar.
Composición de funciones
Con la composición de funciones conseguimos alcanzar una solución con el
método de divide y venceras y al mismo tiempo reusar código. La
composición de funciones es una técnica de programación en la que se
combinan varias funciones para crear una función más compleja. En
programación funcional las funciones gigantes se evitan.
fun alCuadrado(numero:Int)= numero*numero
fun sumaDeCuadrados(num1:Int,num2:Int)= alCuadrado(num1)+alCuadrado(num2)
fun cuadradoAntecesorMasSucesor(numero:Int)= sumaDeCuadrados(numero-1,numero+1)
print(cuadradoAntecesorMasSucesor(3))
20
Funciones recursivas
ya conocemos está técnica que es vital para programación funcional ya
que permite resolver problemas evitando bucles y razonando a un nivel de
abstracción superior, es decir, razonamientos más alejados de la
arquitectura hardware.
Funciones de orden superior(high order) y funciones de primera clase (first class)
Los términos first class function (funcion de primera clase ) y high
order function (orden superior) pueden ser confusos. Nosotros siguiendo
a kotlin.org obtenemos las siguientes definiciones.
Función de orden superior. Función que toma como parámetro una
función y/o que devuelve una otra función.
Funcion de primera clase. Las funciones de un lenguaje se consideran
de primera clase si pueden ser de orden superior y además se les
permite ser almacenadas en variables y estructuras de datos. Con
este término nos referimos por tanto a la capacidad del lenguaje
para hacer esto, por supuesto que no es obligatorio que todas las
funciones se asignen a variables, tengan como parámetro una función
o devuevan una función.
Para liar más a las funciones de primera clase también se les llama de
primer orden, término que fácilmente se confunde con orden superior.
Un ejemplo con funciones high order
El paso de funciones por parámetro de una función y la devolución de una
función ya fue estudiada anteriormente. Observamos en el siguiente
ejemplo que calculateCost() y getDiscount() son high order functions
classProduct(val name: String, val price: Double)
val shippingCost = 4.0val taxRate = 0.08//calculateCost es una high order porque su segundo parámetro es una funciónfun calculateCost(product: Product, discount: (Double) -> Double): Double {
val subtotal = discount(product.price) + shippingCost
val tax = subtotal * taxRate
return subtotal + tax
}
//getDiscount es una high order porque devuelve una función, una de las lambdas del whenfun getDiscount(discountCode: String): (Double) -> Double {
return when (discountCode) {
"10%_OFF"-> { p -> p * 0.9 }
"5_BUCKS_OFF"-> { p -> Math.max(p - 5.0, 0.0) }
else-> { p -> p }
}
}
val product = Product("Widget", 10.0)
println(calculateCost(product, getDiscount("10%_OFF")))
println(calculateCost(product, getDiscount("5_BUCKS_OFF")))
14.04
9.72
Funciones de orden superior en la librería standard
Para enrqiuecer la capacidad de programación funcional en Kotlin, la
librería standar tiene un buen número de funciones de orden superior.
Veamos como ejemplos Repeat() y takeif(). Recuerda que los bucles y
los if de la programación imperativa no pertenecen al estilo funcional
de escribir programas. repeat() será la función que permite conseguir un
efecto equivalente a los bucles imperativos y takeif() al if. Hay un
momento para cada cosa, hoy por hoy ambos estilon son importantes y los
vamos a encontrar mezclados dentro de una aplicación si fue echa con un
estilo de programación actualizado.
repeat()
Recuerda que en el notebook anterior indicamos la sintaxis especial para
invocar funciones cuyo último argumento es una función lambda
Si consultas en la documentación de Kotlin la función repeat()
observarás que tiene la siguiente firma
repeat(times: Int, action: (Int) -> Unit)
Fíjate mucho en el segundo parámetro action, su tipo es una “tipo
función”, por lo tanto, cuando se invoque a esta función se le pasará
como segundo argumento una función y por tanto repeat() es una funcion
de orden superior.
El funcionamineto de repeat() es el siguiente, el primer parámetro,
times, es el número de veces que se debe ejecutar la función que
recibirá el segundo parámetro.
En definitva repeat() es una manera de iterar pero con un estilo más
funcional, en lugar de escribir un bucle, invocamos a la función
repeat().
El parámetro Int de la función te puede resultar confuso no le des
importancia, simplemente es un índice de la iteración que podemos usar
en el cuerpo de la actión como vemos en el último ejemplo.
repeat(3, { println("Hola") })
repeat(3) { println("Adios") } //MEJOR ASÍ con código lambda fuera de paréntesis// greets with an indexrepeat(3) { index -> println("Hello with index $index")
}
Hola
Hola
Hola
Adios
Adios
Adios
Hello with index 0
Hello with index 1
Hello with index 2
takeif()
si consultamos la doc de kotlin
inline fun <T> T.takeIf(predicate: (T) -> Boolean): T?
Observamos que:
es genérica
se usa como función miembro por extensión, por ejemplo como miembro
de Int o String
su parámetro es una función
En el sigueinte ejemplo observamos que takeIf devuelve el número sobre
el que se aplica si la función lambda del parámetro devuelve true, en
caso contrario takeif() devuelve null
// Ejemplo con takeIf() miembro de Intvar numero = 5
var numeroConCondicion = numero.takeIf({ it < 10 })
println(numeroConCondicion)
numero=12
numeroConCondicion = numero.takeIf { it < 10 }
println(numeroConCondicion)
5
null
Se puede también aplicar también por ejemplo a Strings
val miNombre="Chuly Pachuly"val nombreConCondicion= miNombre.takeIf { it.length<20 }
println(nombreConCondicion)
Chuly Pachuly
Realmente como takeIf() es una función genérica (observa la firma
indicada más arriba), se puede aplicar a cualquier tipo, por ejemplo a
un tipo personalizado Persona.
Observa que en el siguiente ejemplo , se utiliza el operador de
navegación segura (?.) para acceder al nombre de esMayorDeEdad1. Esto se
debe a que esMayorDeEdad1 es potencialmente nulo ya que takeif puede
devolver null. Si la persona a la que hace referencia esMayorDeEdad1 no
es mayor de edad (es decir, si edad es menor que 18), entonces
esMayorDeEdad1 será null. En ese caso, si intentamos acceder al nombre
directamente, nos encontraríamos con un error de NullPointerException.
Para evitar este error, utilizamos el operador de navegación segura (?.)
en la línea println(esMayorDeEdad1?.nombre). Esto significa que si
esMayorDeEdad1 es null, la expresión se evaluará como null en lugar de
lanzar una excepción.
En la línea println(esMayorDeEdad2), no necesitamos utilizar el operador
de navegación segura porque no estamos intentando acceder a un miembro
de objeto en esMayorDeEdad2. En su lugar, simplemente imprimimos el
valor de esMayorDeEdad2, que puede ser nulo si la persona no es mayor de
edad.
classPersona(val nombre: String, val edad: Int)
val persona1 = Persona("Juan", 20)
val persona2 = Persona("Ana", 16)
val esMayorDeEdad1 = persona1.takeIf { it.edad>= 18 }
val esMayorDeEdad2 = persona2.takeIf { it.edad>= 18 }
println(esMayorDeEdad1?.nombre)//necesitamos el operador ? por si contiene nullprintln(esMayorDeEdad2)
Juan
null
Un ejemplo combinado repeat() y takeif()
El ejemplo utiliza el operador elvis ?: de tal forma que cuando takeIf
devuelve el valor null se sustituye por la cadena “No es par”
repeat(5) {
println((1..10).random().takeIf { it % 2 == 0 } ?: "No es par")
}
No es par
4
No es par
4
No es par
Funciones de orden superior para trabajar con colecciones y sequence
Manipular los datos de colecciones y sequence es un contexto que se
adapta muy bien para el estilo de programación funcional. Lo vemos un
notebook posterior.
funciones de orden superior y colecciones
FUNCIONES DE ORDEN SUPERIOR PARA TRABAJAR CON COLECCIONES
Simplemente pretendemos ver algunas posibilidades para intuir como
procesar una colección con la ayuda de las funciones de orden superior.
Podremos usar un gran número de funciones de orden superior de la
librería standar o escribir otras nuevas personalizadas a nuestras
necesidades. En este documento nos limitamos a ver ejemplos con algunas
de las funciones de orden superior más comunes de la librería standard y
manejadas de forma sencilla e intuitiva ya que su uso en profundidad
requiere más dedicación que la que aquí se le presta.
Ya debió quedar claro de los documentos anteriores que todas estas
funciones por ser de orden superior aceptaran una función como parámetro
y/o devolveran una función como resultado.
Observaras que muchas de estas funciones iteran automáticamente sobre la
colección elemento a elemento lo que no spermite eliminar la setencia
for o while del código para conseguir así un estilo más funcional.
map
Toma una colección y una función de transformación como parámetros, y
devuelve una nueva colección que resulta de aplicar la función a cada
elemento de la colección original.
val numeros = listOf(1, 2, 3, 4, 5)
val numerosCuadrados = numeros.map { it * it }
println(numerosCuadrados)
[1, 4, 9, 16, 25]
filter
Toma una colección y una función de predicado como parámetros, y
devuelve una nueva colección que contiene únicamente los elementos de la
colección original que cumplen con el predicado.
val numeros = listOf(1, 2, 3, 4, 5)
val numerosPares = numeros.filter { it % 2 == 0 }
println(numerosPares)
[2, 4]
fold
Toma una colección, un valor inicial y una función de acumulación como
parámetros, y devuelve el resultado de aplicar la función de acumulación
a cada elemento de la colección, empezando por el valor inicial.
val words = listOf("Hola", " ", "Mundo", "!")
val concatenated = words.fold("") { acc, word -> acc + word
}
println(concatenated)
Hola Mundo!
foreach
Se ejecuta la acción indicada por la lambda para cada elemento de la
colección
val numeros = listOf(1, 2, 3, 4, 5)
numeros.forEach { numero -> println(numero)
}
1
2
3
4
5
takewhile
si foreach lo puedes ver como una suerte for imperativo automático que
recorre toda la colección, takewhile lo podemos ver como un while
automático que recorre la colección hasta que deja de cumplirse una
condición
val numeros = listOf(1, 2, 3, 4, 5)
val numerosTomados = numeros.takeWhile { it < 4 }
println(numerosTomados)
[1, 2, 3]
groupBy
Toma una colección y una función de transformación como parámetros, y
devuelve un mapa que agrupa los elementos de la colección por el
resultado de aplicar la función de transformación a cada elemento.
data classPersona(var nombre:String,var edad:Int) //recuerda que data class ya provee de toString()val personas = listOf(
Persona("Juan", 25),
Persona("María", 30),
Persona("Pedro", 25),
Persona("Lucía", 30)
)
val personasPorEdad = personas.groupBy { it.edad }
println(personasPorEdad)
Es un tema extenso y tiene muchas posibilidades. Como intuimos habrá
funciones de alto orden relacionadas con sorting.
Nos restringimos aquí a ver un ejemplo escrito de una manera
característica del estilo kotlin. En el ejemplo observamos:
podemos usar sortedBy() si los elementos de la colección son
comparables
si los elementos no son comparables o quiero otro orden indico un
comparador con sortedWith()
Se puede generar un comparador para el típico orden por campos con
compareBy() especificando una lambda por campo requerido para
ordenar.
classFullName(val name: String, val surname: String) {
override fun toString(): String ="$name $surname"}
val names = listOf(
FullName("B", "B"),
FullName("B", "A"),
FullName("A", "A"),
FullName("A", "B"),
)
println(names.sortedBy { it.name })
// [A A, A B, B B, B A]println(names.sortedBy { it.surname })
// [B A, A A, B B, A B]println(names.sortedWith(compareBy(
{ it.surname },
{ it.name }
)))
// [A A, B A, A B, B B]println(names.sortedWith(compareBy(
{ it.name },
{ it.surname }
)))
//[A A, A B, B A, B B]
[A A, A B, B B, B A]
[B A, A A, B B, A B]
[A A, B A, A B, B B]
[A A, A B, B A, B B]
funciones de orden superior y sequence
FUNCIONES DE ORDEN SUPERIOR Y SEQUENCE
que es sequence y en que se diferencia de las colecciones
Una sequence es una secuencia de elementos similar en muchos aspectos a
una lista o a un array. Así por ejemplo una lista Kotlin y una sequence
tienen muchos métodos en común como : map, filter, reduce, take, drop,
flatten, count, distinct, entre otras.
También hya diferencias y la difencia más importante, por sus
consecuencias, es que los elementos de una secuencia pueden ser
procesados de manera perezosa (lazy), a medida que se van necesitando,
de tal forma que en una Sequence no se calculan todos los elementos de
antemano, sino que se calculan bajo demanda, en el momento justo en que
se necesitan. Esto puede tener una gran ventaja en términos de
eficiencia y uso de recursos cuando se trabaja con secuencias grandes o
complejas.
Hay muchas formas de obtener una Sequence. Una sencilla es a partir de
una colección ya existente y esto se puede hacer de muchas formas, como
por ejemplo con la función asSequence()
Una vez que tenemos una Sequence, podemos aplicar una serie de
operaciones para procesar los elementos de manera perezosa, de forma
similar a lo que podemos hacer con las colecciones “tradicionales” pero
con otro rendimiento y otras posibilidades de computación.
val lista = listOf(1, 2, 3, 4, 5)
val secuencia = lista.asSequence()
.filter { it % 2 == 0 } // Filtramos los números pares .map { it * 2 } // Duplicamos cada número .sortedDescending() // Ordenamos en orden descendente println("Con sequence")
secuencia.forEach { println(it) }
//desde punto de vista de operaciones idem si lo hacemos directamente con la lista val listaProcesada= lista.filter { it % 2 == 0 }
.map { it * 2 }
.sortedDescending()
println("Directamente con lista")
listaProcesada.forEach { println(it) }
Con sequence
8
4
Directamente con lista
8
4
UD 1. Acceso a ficheros, flujos, serialización de objetos, ficheros JSON y XML
UD 1. Acceso a ficheros, flujos, serialización de objetos, ficheros JSON y XML
XML con Java: procesadores DOM y SAX, las clases específicas para el tratamiento de la información contenida en un fichero XML, las clases específicas para la vinculación de objetos, las bibliotecas para conversión de documentos XML a otros formatos.
Gestión de información almacenada en ficheros, flujos, haciendo especial hincapié en los formatos JSON y algo de XML mediante aplicaciones informáticas escritas en Java.
a) Gestión de flujos, ficheros secuenciales, Acceso Directo y Directorios: desarrollo de aplicaciones que gestionan información almacenada en ficheros secuenciales, de acceso directo y en el sistema de directorios. En ella se aprenderá a identificar y utilizar las clases específicas para operar con cada tipo de fichero y con el sistema de directorios y a manejar las excepciones para el tratamiento de los posibles errores.
b) Gestión de ficheros JSON y, en menor medida, XML: desarrollo de aplicaciones que gestionan información almacenada en ficheros JSON (con biblioteca Gson) y una introducción a Moshi. También veremos algo de XML, y prenderemos a utilizar los procesadores DOM y SAX, las clases específicas para el tratamiento de la información contenida en un fichero XML, las clases específicas para la vinculación de objetos, las bibliotecas para conversión de documentos XML a otros formatos y a manejar las excepciones para el tratamiento de los posibles errores.
Subsecciones de UD 1. Acceso a ficheros, flujos, serialización de objetos, ficheros JSON y XML
01.01 Java IO. Acceso a ficheros, flujos, serialización de objetos.
UD 01.01. Java IO. ficheros y flujos
En este apartado estudiaremos las principales clases y métodos de la API de Java para el acceso a ficheros y flujos de datos:
El API Java IO proporciona clases para entrada y salida a través de flujos de datos, serialización y sistemas de ficheros (leer y escribir datos en archivos, así como para leer y escribir datos en la consola).
En este apartado estudiaremos cómo se organizan los archivos y directorios en un sistema de archivos y cómo acceder a ellos con la clase java.io.File (el modo tradicional de hacerlo).
Luego veremos cómo leer y escribir datos de archivo con las clases de flujo (Streams IO, no confundir con la API Streams).
Concluimos discutiendo formas de leer la entrada del usuario en tiempo de ejecución utilizando la clase Console.
Lectura y escritura de datos por consola y archivos, empleando flujos de I/O (modo “tradicional”)
Uso de flujos de E/S para la lectura y escritura de archivos.
Lectura y escritura de objetos por medio de serialización (Serializable)
Subsecciones de 01.01 Java IO. Acceso a ficheros, flujos, serialización de objetos.
Las aplicaciones Java, ¿qué pueden hacer fuera del ámbito de gestionar objetos y atributos en la memoria? ¡Al cerrar el programa se pierde todo! ¿Cómo pueden guardar datos para que la información no se pierda cada vez que el programa se termina? ¡Usar archivos, por supuesto!, es la primera opcion (o cualquier sistema de persistencia más avanzado, como bases de datos, que abordaremos en la siguiente unidad).
Se pueden realizar programas sencillos que guarden el estado actual de una aplicación en un archivo cada vez que la aplicación se cierra y luego cargue los datos cuando se ejecute la aplicación la próxima vez. De esta manera, la información se preserva entre ejecuciones del programa. Es lo que se denomina, persistencia.
Este apartado estudiaremos el API java.io para interactuar con archivos y flujos. Comenzamos describiendo cómo se organizan los archivos y directorios en un sistema de archivos y mostramos cómo acceder a ellos con la clase java.io.File (el modo tradicional de hacerlo). Luego veremos cómo leer y escribir datos de archivo con las clases de flujo (Streams IO, no confundir con la API Streams). Concluimos discutiendo formas de leer la entrada del usuario en tiempo de ejecución utilizando la clase Console.
En el siguiente apartado, dedicado a “Java NIO.2”, veremos cómo Java proporciona técnicas más poderosas (y rápidas) para gestionar archivos.
2. Archivos y directorios
Comenzamos este apartado repasandoqué es un archivo y un directorio en un sistema de archivos. También presentamos la clase java.io.File y veremos cómo usarla para leer y escribir información de archivos.
2.1. Sistema de Archivos
Para empezar es necesario saber qué es un sistema de archivos. Los datos se almacenan en dispositivos de almacenamiento persistentes, como discos duros o tarjetas de memoria, por ejemplo.
Un archivo es un registro dentro del dispositivo de almacenamiento que contiene datos.
Los archivos se organizan en jerarquías utilizando directorios.
Un directorio es una ubicación que puede contener archivos y otros directorios.
Cuando trabajamos con directorios en Java, a menudo los tratamos como archivos. De hecho, se usan muchas de las mismas clases para operar en archivos y directorios. Por ejemplo, un archivo y un directorio pueden renombrarse con el mismo método de Java.
Para interactuar con archivos, necesitamos conectarnos al sistema de archivos. El sistema de archivos se encarga de leer y escribir datos en un ordenador. Los diferentes sistemas operativos utilizan sistemas de archivos diferentes para gestionar sus datos. Por ejemplo, los sistemas basados en Windows usan un sistema de archivos diferente que los basados en Unix (Linux, …). La JVM se conectará automáticamente al sistema de archivos local, lo que te permite realizar las mismas operaciones en múltiples plataformas.
Directorio raíz
El directorio raíz (root) es el directorio superior en el sistema de archivos, del cual heredan todos los archivos y directorios:
En Windows, se denota con una letra de unidad, como c:\\.
En Linux se denota con una barra diagonal simple, /.
Rutas
Una ruta es una representación en cadena de un archivo o directorio dentro de un sistema de archivos. Cada sistema de archivos define su propio carácter separador de rutas que se utiliza entre las entradas de directorio. El valor a la izquierda de un separador es el padre del valor a la derecha del separador. Por ejemplo, el valor de ruta /home/otto/cole.txt significa que el archivo cole.txt está dentro del directorio otto, con el directorio otto dentro del directorio home.
En la figura anterior muestra un árbol de directorios de ejemplo que contiene un único nodo raíz. Microsoft Windows admite varios nodos raíz. La familia de sistemas operativos basados en Unix (Linux, Solaris, macOS, etc.) admite un único nodo raíz, que se indica mediante el carácter de barra diagonal.
Un archivo se identifica por su ruta en el sistema de ficheros, empezando por el nodo raíz. Por ejemplo, en el sistema de ficheros de Windows, la ruta C:\Programas\holamundo.kt identifica un archivo llamado holamundo.kt que se encuentra en el directorio Programas en la unidad C:.
En la figura: /home/sally/statusReport y c:\home\sally\statusReport son rutas absolutas pa SO Unix y Windows, respectivamente.
El delimitador es específico del sistema de archivos. En Linux \ y en Windows /.
2.1. Almacenar Datos como Bytes
Los datos se almacenan en un sistema de archivos (y en la memoria) como un 0 o 1, llamado bit. Dado que es realmente difícil para las personas leer/escribir datos que son sólo 0s y 1s, se agrupan en un conjunto de 8 bits, llamado byte.
¿Qué pasa con el tipo primitivo byte de Java? Como veremos en el apartado de flujos de E/S, a menudo se leen o escriben valores en flujos utilizando valores de byte y arrays de bytes, si bien los métodos recogerán valores enteros para el control de fin de flujo o lectura/escritura.
Caracteres ASCII
Usando un poco de aritmética (2^8), vemos que un byte se puede establecer en uno de 256 posibles permutaciones. Estos 256 valores forman el alfabeto básico del Sistema Informático para poder escribir caracteres como a, # y 7. Históricamente, los 256 caracteres se conocen como caracteres ASCII, basado en el estándar de codificación que los definió. Teniendo en cuenta todos los idiomas (como galego e castelán) y emojis disponibles hoy en día, 256 caracteres es realmente restrictivo. Se han desarrollado muchos estándares más nuevos que se basan en bytes adicionales para mostrar caracteres.
1. Clases para trabajar con ficheros (java.io.File, RandomAccessFile, …)
Los flujos de entrada/salida (streams I/O), que veremos en esta unidad, trabajan con gran variedad de fuentes de datos, incluyendo archivos, sin embargo, los flujos no proporcionan todas las operaciones comunes a los archivos de disco.
Existen clases de E/S para trabajar con ficheros que no son orientas a flujos. Algunas de ellas son:
java.nio.file.Path: interface añadida en Java 7 y que permite una forma de trabajar con rutas de archivos y directorios más eficiente. Esta interfaz se emplea con la clase Files para proporcionar un uso más eficiente y completo para acceder a operaciones adicionales, como atributos de archivos, o excepciones de E/S que ayudan a diagnosticar problemas de E/S.
java.nio.file.Files: clase dispone de métodos estáticos para operaciones de archivos y directorios, así como creación de flujos de entrada/salida.
La clase File
La primera clase que estudiaremos es una de las más empleadas (y antigua) del API java.io: la clase java.io.File.
La clase File se utiliza para leer información sobre archivos y directorios existentes, listar el contenido de un directorio o crear/eliminar archivos y directorios.
Una instancia de una clase File representa la ruta a un archivo o directorio específico en el sistema de archivos, pero no contiene los datos del archivo o directorio (el archivo podría no existir).
La clase File no puede leer ni escribir datos dentro de un archivo, aunque se puede pasar como referencia a muchas clases de flujos (y métodos) para leer o escribir datos. Para escribir leer datos de un archivo, se utilizan las clases de flujo de E/S: FileInputStream, FileOutputStream, FileReader, FileWriter, RandomAccessFile, etc.
Por ello, se usa para convertir el nombre de un archivo y pasarlo como parámetro a otros métodos o constructores que sí pueden leer o escribir datos.
FileChannel
La clase FileChannel, del API Java NIO, de java.nio.channels proporciona una forma más avanzada de trabajar con archivos que RandomAccessFile. Tanto File como FileChannel funcionan, pero para trabajar con puro Java NIO debe usarse la clase FileChannel.
java.nio.file.Files
La clase java.nio.file.Files proporciona únicamente métodos estáticos para operaciones de archivos y directorios, así como creación de flujos de entrada/salida. Es más eficiente que la clase File y se recomienda su uso en lugar de Filepara nuevas aplicaciones.
2. Creación de un Objeto File
Un objeto File a menudo se inicializa con una cadena que contiene una ruta absoluta o relativa al archivo o directorio en el sistema de archivos.
La ruta absoluta de un archivo o directorio es la ruta completa desde el directorio raíz hasta el archivo o directorio, incluyendo todos los subdirectorios que contienen el archivo o directorio.
La ruta relativa de un archivo o directorio es la ruta desde el directorio de trabajo actual hasta el archivo o directorio. Por ejemplo, lo siguiente es una ruta absoluta al archivo javaio.txt:
/home/otto/apuntes/javaio.txt
Lo siguiente es una ruta relativa al mismo archivo, asumiendo que el directorio actual del usuario está configurado en /home/otto:
apuntes/javaio.txt
Los sistemas operativos diferentes varían en su formato de nombres de ruta. Por ejemplo, los sistemas basados en Unix usan la barra diagonal hacia adelante, /, para las rutas, mientras que los sistemas basados en Windows usan el carácter diagonal inversa, \, como separador de ruta.
Muchos lenguajes de programación y sistemas de archivos admiten ambos tipos de barras al escribir declaraciones de ruta. Por conveniencia, Java ofrece dos opciones para recuperar el carácter separador local: una propiedad del sistema y una variable estática definida en la clase File. Ambos ejemplos imprimirán el carácter separador para el entorno actual:
El primero crea un objeto File a partir de una ruta en forma de cadena. Los otros dos constructores se utilizan para crear un objeto File a partir de una ruta principal y una secundaria, como la siguiente:
En este ejemplo, creamos dos nuevas instancias de File que son equivalentes la instancia anterior de apuntesJavaIO. Si la instancia principal es nula, se omitiría y el método volvería al constructor de cadena única.
Constructores de la clase File
Constructor
Descripción
File(String pathname)
Crea un objeto File a partir de una ruta en forma de cadena.
File(File parent, String child)
Crea un objeto File a partir de una ruta principal y una secundaria.
File(String parent, String child)
Crea un objeto File a partir de una ruta principal y una secundaria.
File(URI uri)
Crea un objeto File a partir de un URI.
Campos de la clase File
La clase File tiene varios campos que puedes usar para acceder a información sobre el sistema de archivos subyacente. Algunos de los campos más útiles son:
Campo
Descripción
static String pathSeparator
El separador de PATH de la plataforma. Por ejemplo, en Windows es ; y en Unix es :.
static char pathSeparatorChar
El separador de ruta de la plataforma como un carácter.
static String separator
El separador de ruta de la plataforma. Por ejemplo, en Windows es \ y en Unix es /.
static char separatorChar
El separador de ruta de la plataforma como un carácter.
3. El objeto File vs. archivo real existente
Al trabajar con una instancia de la clase File, ten en cuenta que sólo representa una ruta a un archivo. A menos que se opere sobre él, no está conectado a un archivo real en el sistema de archivos.
Por ejemplo:
Se puede crear un nuevo objeto File para comprobar si un archivo existe en el sistema.
Se puede llamar a varios métodos para leer propiedades de archivos dentro del sistema de archivos.
Tiene hay métodos para modificar el nombre o la ubicación de un archivo, así como para eliminarlo.
La JVM y el sistema de archivos subyacente leerán o modificarán el archivo utilizando los métodos que llamas en la clase File. Si intentas operar en un archivo que no existe o al que no tienes acceso, algunos métodos de File lanzarán una excepción. Otros métodos devolverán false si el archivo no existe o la operación no se puede realizar.
4. Trabajando con un Objeto File
La clase File contiene numerosos métodos útiles para interactuar con archivos y directorios en el sistema de archivos. En la siguiente tabla se muestran los más importantes, por su uso:
5. Métodos más importantes de java.io.File
Nombre del Método
Descripción
boolean delete()
Borra el archivo o directorio y devuelve true sólo si la operación se completó con éxito. Si esta instancia es un directorio, el directorio debe estar vacío para poder eliminarse.
boolean exists()
Comprueba si un archivo existe
String getAbsolutePath()
Obtiene el nombre absoluto del archivo o directorio en el sistema de archivos
String getName()
Obtiene el nombre del archivo o directorio
String getParent()
Obtiene el directorio principal en el que se encuentra la ruta, o null si no hay ninguno
boolean isDirectory()
Comprueba si una referencia File es un directorio en el sistema de archivos
boolean isFile()
Comprueba si una referencia File es un archivo en el sistema de archivos
long lastModified()
Devuelve el número de milisegundos desde la época (número de milisegundos desde las 12 a.m. UTC del 1 de enero de 1970) en que se modificó el archivo por última vez
long length()
Obtiene el número de bytes en el archivo
File[] listFiles()
Obtiene una lista de archivos dentro de un directorio
boolean mkdir()
Crea el directorio especificado en la ruta
boolean mkdirs()
Crea el directorio especificado en la ruta, incluyendo cualquier directorio principal inexistente
boolean renameTo(File dest)
Cambia el nombre del archivo o directorio denotado por esta ruta a dest y devuelve true sólo si la operación tuvo éxito.
Prueba el siempre útil programa de ejemplo de muestra que muestra información sobre un archivo o directorio, como si existe, qué archivos contiene y más:
El siguiente es un programa de ejemplo de muestra que, dado una ruta a un archivo, muestra información sobre el archivo o directorio, si existe, qué archivos contiene y más:
var arquivo =new File("c:\\home\\otto\\noHayCole.txt");
System.out.println("Archivo existe: "+ arquivo.exists());
if (arquivo.exists()) {
System.out.println("Ruta absoluta: "+ arquivo.getAbsolutePath());
System.out.println("Es un directorio: "+ arquivo.isDirectory());
System.out.println("Ruta padre: "+ arquivo.getParent());
if (arquivo.isFile()) {
System.out.println("Tamaño: "+ arquivo.length());
System.out.println("Última modificación: "+ arquivo.lastModified());
} else {
for (File subArquivo : arquivo.listFiles()) {
System.out.println(" "+ subArquivo.getName());
}
}
}
Si la ruta proporcionada no apuntara a un archivo, produciría la siguiente salida:
Archivo existe: false
Si la ruta proporcionada apuntara a un archivo válido, produciría algo similar a lo siguiente:
Archivo existe: true
Ruta absoluta: c:\home\otto\noHayCole.txt
Es un directorio: false
Ruta padre: c:\home\otto
Tamaño: 14883
Última modificación: 1806860000003
Finalmente, si la ruta proporcionada apuntara a un directorio válido, como c:\home, produciría algo similar a lo siguiente:
Archivo existe: true
Ruta absoluta: c:\home
Es un directorio: true
Ruta padre: c:\
asisoy.txt
noHayCole.txt
zalandomami.txt
En estos ejemplos, ves que la salida de un programa basado en Entrada/Salida depende por completo de los directorios y archivos disponibles en tiempo de ejecución en el sistema de archivos subyacente.
Directorio o archivo
Ojo, /home/otto/noHayCole.txtpodría ser un archivo o un directorio, incluso si tiene una extensión de archivo. ¡No asumas que es uno u otro a menos que lo puedas comprobar! (por ejemplo, .git)
Ejercicios
Ejercicio 1. Creación y lectura de archivos con File
Debes trabajar únicamente con métodos de la clase File.
Realiza los siguientes pasos:
Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
Escribe un programa que cree un objeto File para el archivo prueba.txt y compruebe si el archivo existe.
Si el archivo existe, muestra la ruta absoluta, nombre del archivo, tamaño, última modificación y si es un directorio.
Si el archivo no existe, muestra un mensaje que lo indique y crea uno temporal.
Ejercicio 2. Mostrar el contenido de un directorio
Debes trabajar únicamente con métodos de la clase File.
El programa abre una ventana para la selección de un directorio (hazlo también desde teclado si recoge un parámetro) y usando el método listFiles() de la clase File, muestra el contenido de ese directorio, indicando el tamaño de los archivos y si es un directorio o no.
Además, muestra el tamaño total de los archivos y directorios.
Muestra en una ventana emergente el resultado y por consola.
A continuación puedes ver algunas soluciones parciales del ejercicio 2. Completa el ejercicio de acuerdo a las indicaciones.
El siguiente ejemplo muestra cómo mostrar el contenido de un directorio, haciendo uso de la clase que veremos BufferesReader (no Scanner) para la lectura de teclado:
// Programa Java que muestra todo el contenido de un directorioimport java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
// Mostrando el contenido de un directorioclassContents {
publicstaticvoidmain(String[] args)
throws IOException
{
// Introducimos la ruta y el nombre del directorio por teclado: BufferedReader br =new BufferedReader(new InputStreamReader(System.in));
System.out.println("Introduce la ruta:");
String dirpath = br.readLine();
System.out.println("Introduce el nombre del directorio:");
String dname = br.readLine();
// creamos un objeto File a partir de la ruta y el nombre del directorio File f =new File(dirpath, dname);
// si el directorio existe, mostramos su contenidoif (f.exists()) {
// obtenemos el contenido en un arr[]// el array arr[i] representa el nombre cada archivo o directorio String arr[]= f.list();
// Número de entradas en el directorioint n = arr.length;
// mostramos cada una de las entradas.for (int i = 0; i < n; i++) {
System.out.println(arr[i]);
// Creamos un objeto File para cada entrada y // comprobamos si es un archivo o un directorio. File f1 =new File(arr[i]);
if (f1.isFile())
System.out.println(": es un archivo");
if (f1.isDirectory())
System.out.println(": es un directorio");
}
System.out.println("El directorio no tiene entradas "+ n);
}
else System.out.println("Directorio no encontrado");
}
}
Ejercicio 3. Gestor de archivos y directorios
Como en todos los ejercicios anteriores, debes trabajar únicamente con métodos de la clase File.
Escribe un programa en Java que funcione como un gestor básico de archivos y directorios. El programa debe permitir al usuario realizar las siguientes operaciones:
Crear un directorio, empleando la clase JFileChooser para seleccionar la ruta donde se creará.
Listar todos los archivos y subdirectorios de un directorio de forma recursiva.
Eliminar un archivo o directorio. Si es un directorio, eliminar todo su contenido de forma recursiva.
Mover o renombrar archivos y directorios.
El programa debe ofrecer un menú para que el usuario elija la operación que desea realizar. La selección de directorios o archivos debe realizarse con la clase JFileChooser.
6. Nuevas características del paquete java.nio.file
Aunque la clase java.io.File es útil para muchas operaciones de E/S de archivos, Java SE 7introdujo una nueva API de E/S de archivos en el paquete java.nio.file que proporciona una funcionalidad más rica y más eficiente para trabajar con archivos y directorios. Este modo de hacerlo lo veremos en el siguiente apartado.
Antes del lanzamiento de Java SE 7, la clase java.io.File era el mecanismo utilizado para la E/S de archivos, pero presentaba varios inconvenientes:
Muchos métodos no lanzaban excepciones al fallar, por lo que era imposible obtener un mensaje de error útil. Por ejemplo, si fallaba la eliminación de un archivo, el programa recibía un “fallo al eliminar”, pero no sabía si era porque el archivo no existía, el usuario no tenía permisos, o había algún otro problema.
El método rename no funcionaba de manera consistente en diferentes plataformas.
No había un soporte real para enlaces simbólicos.
Se requería más soporte para metadatos, como permisos de archivos, propietario del archivo y otros atributos de seguridad.
El acceso a los metadatos de los archivos era ineficiente.
Muchos de los métodos no escalaban bien. Solicitar un listado de directorios grandes en un servidor podía causar bloqueos. Los directorios grandes también podían generar problemas de recursos de memoria, lo que resultaba en una denegación de servicio.
No era posible escribir código fiable que pudiera recorrer un árbol de archivos recursivamente y responder adecuadamente si había enlaces simbólicos circulares.
Aun así, existe mucho código que usa java.io.File y sigue siendo útil para muchas situaciones. Aunque lo veremos al detalle, si quisieras aprovechar la funcionalidad de java.nio.file.Path con el menor impacto posible en tu código muestro ejemplos de ello.
Conversión entre java.io.File y java.nio.file.Path
La clase java.io.File proporciona el método toPath, que convierte una instancia de estilo antiguo en una instancia java.nio.file.Path:
Path entrada = file.toPath();
De esta forma, puedes aprovechar el conjunto de características que ofrece la clase Path.
Por ejemplo, si tuvieras algún código que eliminara un archivo:
file.delete();
Podrías modificar este código para usar el método Files.delete, de la siguiente manera:
Path fp = file.toPath();
Files.delete(fp);
A la inversa, el método Path.toFile construye un objeto java.io.File para un objeto Path.
Mapeo de la Funcionalidad de java.io.File a java.nio.file
Dado que la implementación de la E/S de archivos en Java ha sido completamente re-arquitectada en la versión Java SE 7, no puedes intercambiar un método por otro directamente. Si deseas usar la rica funcionalidad que ofrece el paquete java.nio.file, la solución más sencilla es usar el método File.toPath.
No hay una correspondencia uno a uno entre las dos APIs, pero la siguiente tabla da una idea general de qué funcionalidad en la API java.io.File corresponde a la funcionalidad en la API java.nio.file, y te indica dónde puedes obtener más información.
La clase RandomAccessFile permite acceso no secuencial, o aleatorio, al contenido del archivo.
Permite leer y escribir (implementa las interfaces DataInput y DataOutput) en archivos de acceso aleatorio.
En el constructor se especifica el modo de apertura, lectura o escritura:
new RandomAccessFile("proba.txt", "r"); // Solo lecturanew RandomAccessFile("proba.txt", "rw"); // Lectura y escrituranew RandomAccessFile("proba.txt", "rwd"); // Lectura y escritura, sincronizado
Emplea la notación de puntero a archivo para especificar la posición actual en el archivo.
Al crearlo apunta al principio del archivo, la posición 0.
Las sucesivas llamadas a read o writemodifican la posición del puntero el número de bytes leídos o escritos, respectivamente.
Dispone de 3 métodos para modificar la posición del puntero:
int skipBytes(int n): mueve el puntero hacia delante n bytes.
void seek(long): sitúa el puntero justo antes del byte especificado.
long getFilePointer(): devuelve la posición actual del puntero a archico.
Las instancias de esta clase soportan tanto la lectura como la escritura en un archivo de acceso aleatorio. Un archivo de acceso aleatorio se comporta como un gran array de bytes almacenado en el sistema de archivos. Existe un tipo de cursor, o índice en el array implícito, llamado puntero de archivo; las operaciones de entrada leen bytes comenzando en el puntero de archivo y avanzan el puntero más allá de los bytes leídos.
Si el archivo de acceso aleatorio se crea en modo de lectura/escritura, entonces las operaciones de salida también están disponibles; las operaciones de salida escriben bytes comenzando en el puntero de archivo y avanzan el puntero más allá de los bytes escritos. Las operaciones de salida que escriben más allá del final actual del array implícito causan que el array se extienda.
El puntero de archivo se puede leer mediante el método getFilePointer y establecer mediante el método seek.
Para todas las rutinas de lectura en esta clase que, si se alcanza el final del archivo antes de que se haya leído el número deseado de bytes, se lanza una excepción EOFException (que es un tipo de IOException).
Si no se puede leer ningún byte por alguna razón que no sea el final del archivo, se lanza una IOException distinta a EOFException. En particular, puede lanzarse una IOException si el flujo ha sido cerrado.
Ahora veremos cómo escribir y editar dentro de un archivo existente, en lugar de solo escribir en un archivo completamente nuevo o agregar a uno existente. Simplemente: necesitamos acceso aleatorio.
RandomAccessFile nos permite escribir en una posición específica del archivo, dado el desplazamiento (offset) desde el principio del archivo en bytes.
Este código escribe un valor entero con un desplazamiento dado desde el principio del archivo:
privatevoidwriteToPosition(String filename, int data, long position)
throws IOException {
RandomAccessFile writer =new RandomAccessFile(filename, "rw");
writer.seek(position);
writer.writeInt(data);
writer.close();
}
Si queremos leer el entero almacenado en una ubicación específica, podemos usar este método:
privateintreadFromPosition(String filename, long position)
throws IOException {
int result = 0;
RandomAccessFile reader =new RandomAccessFile(filename, "r");
reader.seek(position);
result = reader.readInt();
reader.close();
return result;
}
Para probar nuestras funciones, escribamos un entero, lo editemos, y finalmente lo leamos:
Ejercicio 4. Escritura y lectura de archivos con RandomAccessFile
Escribe un programa que escriba y lea datos en un archivo usando la clase RandomAccessFile.
Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
Escribe un programa que cree un objeto RandomAccessFile para el archivo prueba.txt y escriba un mensaje.
Lee el mensaje y muéstralo por consola.
Ejercicio 5. Escritura y lectura de archivos con RandomAccessFile
Escribe un programa que utilice la clase RandomAccessFile para escribir en un archivo los números del 1 al 10 y luego los lea desde el archivo. Muestra los números leídos en la consola.
Solución al ejercicio 5
import java.io.IOException;
import java.io.RandomAccessFile;
publicclassRandomAccessFileDemo {
publicstaticvoidmain(String[] args) {
try {
RandomAccessFile raf =new RandomAccessFile("prueba.txt", "rw");
for (int i = 1; i <= 10; i++) {
raf.writeInt(i);
}
raf.seek(0);
for (int i = 1; i <= 10; i++) {
System.out.println(raf.readInt());
}
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Ejercicio 6. Modificación de Contenido en un Archivo Binario con RandomAccessFile
Escribe un programa en Java que haga lo siguiente:
Escriba 10 enteros en un archivo llamado “datos.bin”.
Permita al usuario modificar el tercer número almacenado en el archivo por otro número.
Muestra los números antes y después de la modificación en la consola.
Solución al ejercicio 6
import java.io.RandomAccessFile;
import java.io.IOException;
import java.util.Scanner;
publicclassEjercicio3 {
publicstaticvoidmain(String[] args) {
try (RandomAccessFile raf =new RandomAccessFile("datos.bin", "rw")) {
// Escribir 10 enteros en el archivofor (int i = 1; i <= 10; i++) {
raf.writeInt(i);
}
// Leer los números antes de la modificación System.out.println("Números antes de la modificación:");
raf.seek(0);
for (int i = 0; i < 10; i++) {
System.out.println(raf.readInt());
}
// Solicitar al usuario un nuevo número para el tercer número Scanner sc =new Scanner(System.in);
System.out.print("Ingrese un nuevo número para reemplazar el tercer número: ");
int nuevoNumero = sc.nextInt();
// Modificar el tercer número (posición 2 en base 0, cada entero ocupa 4 bytes) raf.seek(2 * 4);
raf.writeInt(nuevoNumero);
// Leer los números después de la modificación System.out.println("Números después de la modificación:");
raf.seek(0);
for (int i = 0; i < 10; i++) {
System.out.println(raf.readInt());
}
} catch (IOException e) {
System.out.println("Ocurrió un error de entrada/salida.");
e.printStackTrace();
}
}
}
La clase RandomAccessFile de Java en la API de Java IO te permite navegar por un archivo y leer o escribir en él según sea necesario. También puedes reemplazar partes existentes de un archivo. Esto no es posible con FileInputStream o FileOutputStream, que veremos en el apartado de flujos de E/S.
1. Creación de un RandomAccessFile
Antes de poder trabajar con la clase RandomAccessFile, debes crear una instancia de esa clase:
Nota el segundo parámetro del constructor, "rw",es el modo en el que quieres abrir el archivo. "rw" significa modo de lectura/escritura..
2. Modos de Acceso
La clase RandomAccessFile de Java soporta los siguientes modos de acceso:
Modo
Descripción
r
Modo de lectura. Llamar a los métodos de escritura lanzará en una IOException.
rw
Modo de lectura y escritura.
rwd
Modo de lectura y escritura - sincrónicamente. Todas las actualizaciones al contenido del archivo se escriben en el disco de manera sincrónica.
rws
Modo de lectura y escritura - sincrónicamente. Todas las actualizaciones al contenido del archivo o metadatos se escriben en el disco de manera sincrónica.
3. Situar el puntero: seek()
Para leer o escribir en una ubicación específica en un RandomAccessFile, primero debes situar el puntero del archivo (también llamado seek) en la posición de lectura o escritura. Esto se hace utilizando el método seek(). Por ejemplo:
Puedes obtener la posición actual de un RandomAccessFile usando su método getFilePointer(). La posición actual es el índice (desplazamiento) del byte en el que el RandomAccessFile está actualmente situado:
long posicion = file.getFilePointer();
5. Lectura de un Byte desde: read()
La lectura un byte desde un RandomAccessFilese realiza usando su método read():
RandomAccessFile file =new RandomAccessFile("c:\\programas\\holamundo.kt", "rw");
int miByte = file.read();
El método read() lee el byte ubicado en la posición del archivo señalada por el puntero en la instancia de RandomAccessFile.
Avance del puntero
Un detalle que el javadoc olvida mencionar: el método read() incrementa el puntero del archivo para que apunte al siguiente byte después del que acaba de ser leído. Esto significa se puede seguir llamando a read() sin tener que mover manualmente el puntero del archivo.
6. Lectura de un array de bytes: read(byte[])
También es posible leer un array de bytes con un RandomAccessFile:
RandomAccessFile randomAccessFile =new RandomAccessFile("programas/datos.txt", "r");
byte[] dest =newbyte[1024]; // Array de bytes donde se almacenarán los datos leídos, llamado buffer.int offset = 0;
int length = 1024;
int bytesLeidos = randomAccessFile.read(dest, offset, length);
Este ejemplo lee una secuencia de bytes en el array de bytes dest pasado como parámetro al método read(). El método read() comenzará a leer en el archivo desde la posición actual del puntero del archivo en el RandomAccessFile.
El método read()comenzará a leer datos en el array de bytes a partir de la posición proporcionada por el parámetro offset, y como máximo el número de bytes proporcionado por el parámetro length.
Este método devuelve el número real de bytes leídos.
7. Escritura de un byte: write()
Puedes escribir un byte en un RandomAccessFileusando su método write(), el cual toma un entero como parámetro. El byte se escribirá en la posición actual del puntero del archivo en el RandomAccessFile. El byte anterior en esa posición será sobrescrito:
Este ejemplo escribe el array de bytes en la posición actual del puntero del archivo en el objeto RandomAccessFile. Cualquier byte que esté en esa posición será sobrescrito con los nuevos bytes.
Al igual que con el método read(), el método write()avanza el puntero del archivo después de ser llamado. De esta manera no tienes que mover constantemente el puntero para escribir datos en una nueva ubicación en el archivo.
También puedes escribir partes de un array de bytes en un RandomAccessFile, en lugar de todo el array:
También puedes cerrar un RandomAccessFile automáticamente si usas la sentencia try-with-resources de Java:
try (RandomAccessFile file =new RandomAccessFile("c:\\programas\\holamundo.kt", "rw")) {
// lectura o escritura en el RandomAccessFile}
Una vez que la ejecución del programa salga del bloque try-with-resources, el objeto RandomAccessFile se cerrará automáticamente, incluso si se lanza una IOException desde dentro del bloque try-with-resources.
Ejemplo completo del uso de RandomAccessFile
En el siguiente ejemplo escribimos una lista de estudiantes pedidos por teclado, guardando el número de estudiantes y el nombre en el mismo archivo. Para la lectura solicitamos el número del estudiante a leer:
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Scanner;
publicclassRegistroEstudiantes {
publicstaticvoidmain(String[] args) throws IOException { // En realidad es mala opción lanzar la excepción, pero es para simplificar el ejemplotry (RandomAccessFile file =new RandomAccessFile("E:\\programas\\estudiantes.txt", "rw")) {
Scanner scanner =new Scanner(System.in);
System.out.println("Introduce el número de estudiantes: ");
int numEstudiantes = scanner.nextInt();
file.writeInt(numEstudiantes);
for (int i = 0; i < numEstudiantes; i++) {
System.out.println("Introduce el nombre del estudiante "+ (i + 1) +": ");
String nombre = scanner.next();
file.writeUTF(nombre);
}
System.out.println("Introduce el número del estudiante a leer: ");
int numEstudiante = scanner.nextInt();
file.seek(0);
int numEstudiantesGuardados = file.readInt();
if (numEstudiante > numEstudiantesGuardados) {
System.out.println("No hay tantos estudiantes guardados.");
} else {
file.seek(4); // Saltamos el número de estudiantesfor (int i = 0; i < numEstudiante - 1; i++) {
file.readUTF();
}
System.out.println("El estudiante "+ numEstudiante +" es: "+ file.readUTF());
}
}
}
}
Ahora que hemos cubierto los conceptos básicos de la clase File, pasemos a los flujos (streams) de E/S, que son mucho más interesantes, pues no sólo pueden emplearse para archivos.
Un flujo de E/S representa una fuente de entrada o un destino de salida. Un flujo puede representar muchos tipos diferentes de fuentes y destinos, incluidos archivos en disco, dispositivos, otros programas, String o arrays de memoria.
En esta sección, veremos cómo usar los flujos de E/S para leer y escribir datos. La “E/S” se refiere a la naturaleza de cómo se accede a los datos, ya sea leyendo los datos desde un recurso (entrada) o escribiendo los datos en un recurso (salida).
Flujos de E/S en Java
En Java, los flujos de E/S se encuentran en el paquete java.io. Aunque Java 9 introdujo un nuevo paquete java.nio.file para operaciones de E/S más avanzadas, java.io sigue siendo ampliamente utilizado y es importante comprenderlo.
Los flujos admiten muchos tipos diferentes de datos, incluidos bytes simples, tipos de datos primitivos, caracteres localizados y objetos. Algunos flujos simplemente transmiten datos; otros manipulan y transforman los datos de formas útiles.
Flujo de entrada
Representan una fuente de entrada.
Pueden proceder de diferentes tipos de fuentes:
Algunos flujos simplemente pasan datos, otros manipulan y transforman los datos.
2. Fundamentos de los flujos de E/S
El contenido de un archivo, una página Web, el teclado, etc. se puede leer o escribir a través de un flujo, que es una lista de elementos de datos presentados secuencialmente. Deberías pensar en los flujos conceptualmente como un “flujo de agua” largo y casi interminable con datos que se presentan uno a uno, como una “ola” a la vez.
En general, el flujo es tan grande que una vez que comenzamos a leerlo, no tenemos idea de dónde comienza o termina. Sólo tenemos un puntero a nuestra posición actual en el flujo y leemos datos bloque por bloque.
Cada tipo de flujo segmenta los datos en una “chorro” o “bloque” de una manera particular. Por ejemplo, algunas clases de flujos leen o escriben datos como bytes individuales. Otras clases de flujos leen o escriben caracteres individuales o cadenas de caracteres. Además, algunas clases de flujos leen o escriben grupos más grandes de bytes o caracteres a la vez, específicamente aquellas con la palabra “Buffered” en su nombre.
Aunque los flujos se utilizan comúnmente con la E/S de archivos, se utilizan de manera más general para manejar la lectura/escritura de cualquier fuente de datos de flujos. Por ejemplo, podrías construir una aplicación Java que envíe datos a un sitio web utilizando un flujo de salida y lea el resultado a través de un flujo de entrada.
Entrada vs Salida
Es importante distinguir entre entrada (InputStream/Reader) y salida (OutputStream/Writer). Es muy sencillo, pues siempre debe verse desde el punto de vista del programa: entrada de datos al programa (lectura) y salida de datos desde el programa (escritura).
3. Nomenclatura de los flujos de E/S
La API java.io proporciona numerosas clases para crear, acceder y manipular flujos, tantas que tienden a abrumar a muchos desarrolladores de Java. ¡Mantén la calma! ;-) Revisaremos las principales diferencias entre cada clase de flujo y veremos cómo distinguirlas. A menudo el nombre del flujo te proporciona suficiente información para comprender exactamente qué hace.
El objetivo de este apartado es familiarizarte con la terminología común y las convenciones de nombres utilizadas con los flujos. No te preocupes si no reconoces los nombres de las clases de flujos particulares que se utilizan aquí; los veremos más adelante y con la práctica se entenderá mejor.
4. Flujos de bytes vs. flujos de caracteres
La API java.io define dos conjuntos de clases de flujos para la lectura y escritura de flujos: flujos de bytes y flujos de caracteres.
4.1. Flujos de bytes (Byte Streams)
Los flujos de bytes leen/escriben datos binarios (0 y 1) y tienen nombres de clase que terminan en InputStream o OutputStream.
Hay muchas clases de flujos de bytes, como: FileInputStream y FileOutputStream. Todos los restantes flujos funcionan del mismo modo sólo difieren en la forma de construirlos.
Los programas utilizan flujos de bytes para realizar la entrada y salida de bytes de 8 bits. Todas las clases de flujos de bytes heredan de InputStream y OutputStream.
4.2. Flujos de caracteres (Character Streams)
Los flujos de caracteres leen/escriben datos de texto y tienen nombres de clase que terminan en Reader o Writer.
Automáticamente, transforma caracteres Unicode (formato de Java) al conjunto de caracteres local.
Java almacena valores de caracteres utilizando convenciones Unicode. La E/S de flujos de caracteres traduce automáticamente este formato interno hacia y desde el conjunto de caracteres local. En locales occidentales, como el juego de caracteres Latin-1 o Windows-1252, el conjunto de caracteres local es generalmente un superconjunto de ASCII de 8 bits. En locales asiáticos, el conjunto de caracteres local es un conjunto de caracteres de doble byte.
5. Flujos de entrada (Input Streams) vs. flujos de salida (Output Streams)
La mayoría de las clases de flujos de entrada tienen una clase de flujo de salida correspondiente, y viceversa. Por ejemplo, la clase FileOutputStream escribe datos que pueden ser leídos por un FileInputStream. Si comprendes las características de una clase de flujo de entrada o salida en particular, naturalmente sabrás qué hace su clase complementaria.
Por lo tanto, la mayoría de las clases Reader tienen una clase Writer correspondiente. Por ejemplo, la clase FileWriter escribe datos que pueden ser leídos por un FileReader. Aunque hay excepciones a esta regla:
La soledad de los flujos de salida PrintWriter y PrintStream
Debes saber que [PrintWriter][printwriter] no tiene una clase PrintReader correspondiente.
Del mismo modo, PrintStream es una OutputStream que no tiene una clase InputStream correspondiente. Tampoco tiene la palabra “Output” en su nombre.
El principal propósito de PrintWriter y PrintStream es facilitar la escritura de datos formateados en un flujo, como sucede con System.out y System.err que son de tipo PrintStream.
Hay muchas clases de flujos de bytes, como: FileInputStream y FileOutputStream. Todos los restantes flujos funcionan del mismo modo sólo difieren en la forma de construirlos.
Los programas utilizan flujos de bytes para realizar la entrada y salida de bytes de 8 bits. Todas las clases de flujos de bytes heredan de InputStream y OutputStream.
Veremos un ejemplo de cómo funcionan los flujos de bytes con flujos de bytes de E/S de archivo, FileInputStream y FileOutputStream. Otros tipos de flujos de bytes se utilizan de manera muy similar; difieren principalmente en la forma en que se construyen.
Ejemplo: copia de archivos
Programa que emplea FileInputStream y FileOutputStream para copiar archivos CopiaArchivos, que utiliza flujos de bytes para copiar un byte a la vez.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
publicclassCopiaArchivos {
publicstaticvoidmain(String[] args) throws IOException {
FileInputStream in =null;
FileOutputStream out =null;
try {
in =new FileInputStream("otto.txt");
out =new FileOutputStream("nohaycole.txt");
int c;
while ((c = in.read()) !=-1) {
out.write(c);
}
} finally { // Hay que cerrar el flujo en cualquier condición.if (in !=null) {
in.close();
}
if (out !=null) {
out.close();
}
}
}
}
CopiaArchivos lee el flujo de entrada y escribe el flujo de salida, un byte a la vez.
El método read() devuelve un valor de byte en forma de un entero, para poder emplear -1 como fin de flujo. Cuando se alcanza el final del archivo, read() devuelve -1.
El método write() escribe un byte en el flujo de salida.
El método close() cierra el flujo. Si no se cierra, el sistema operativo puede no liberar los recursos asociados con el archivo.
Para ficheros de texto (con caracteres, como en el ejemplo) es mejor emplear flujos de caracteres (character streams).
Los flujos de bytes deben usarse sólo para E/S más primitiva (binaria)
Todos los otros tipos de flujos (incluso caracteres) se construyen sobre los flujos de bytes.
Copia de archivos
CopiaArchivos parece un programa normal, pero en realidad representa un tipo de E/S de bajo nivel que debería evitar. Dado que otto.txt contiene datos de caracteres, el mejor enfoque es usar flujos de caracteres, como veremos más adelante. También hay flujos para tipos de datos más complejos. Los flujos de bytes solo deben usarse para la E/S más primitiva.
Entonces, ¿por qué hablar de flujos de bytes?
Porque todos los demás tipos de flujos se construyen sobre flujos de bytes.
Ejercicio 1. Copia de archivos
Modifica el programa CopiaArchivos para que copie el archivo otto.txt en un archivo nohaycole.txt en la carpeta src/main/resources de tu proyecto.
Además, haz que el cierre de archivos se realice por medio de try-with-resources.
Cierre de flujos
Cerrar un flujo cuando ya no se necesita es muy importante. CopiaArchivosutiliza un bloque finally para garantizar que ambos flujos se cierren incluso si se produce un error. Esta práctica ayuda a evitar graves pérdidas de recursos.
La técnica más recomendada es utilizar try-with-resources, que permite que los flujos se cierren automáticamente al final del bloque try:
try (FileInputStream in =new FileInputStream("otto.txt");
FileOutputStream out =new FileOutputStream("nohaycole.txt")) {
int c;
while ((c = in.read()) !=-1) {
out.write(c);
}
}
Un posible error es que CopiaArchivos no pudo abrir uno o ambos archivos. Cuando esto sucede, la variable de flujo correspondiente al archivo nunca cambia desde su valor inicial nulo. Es por eso que CopiaArchivos se asegura de que cada variable de flujo contenga una referencia de objeto antes de llamar a close().
Cuando no usar flujos de bytes
CopiaArchivos parece un programa normal, pero en realidad representa un tipo de E/S de bajo nivel que debería evitar. Dado que otto.txt contiene datos de caracteres, el mejor enfoque es usar flujos de caracteres, como veremos más adelante. También hay flujos para tipos de datos más complejos. Los flujos de bytes solo deben usarse para la E/S más primitiva.
Entonces, ¿por qué hablar de flujos de bytes?
Porque todos los demás tipos de flujos se construyen sobre flujos de bytes.
ByteArrayInputStream: contiene un búfer interno que contiene bytes que pueden ser leídos desde el flujo. Un contador interno lleva un seguimiento del próximo byte que será suministrado por el método read. Cerrar un ByteArrayInputStream no tiene efecto. Los métodos en esta clase pueden ser llamados después de que el flujo haya sido cerrado sin generar una IOException.
AudioInputStream: es un flujo de entrada con un formato de audio y longitud especificados. La longitud se expresa en frames, no en bytes. Se proporcionan varios métodos para leer un cierto número de bytes del flujo, o un número no especificado de bytes. El flujo de entrada de audio lleva un seguimiento del último byte que se leyó. Puedes saltar sobre un número arbitrario de bytes para llegar a una posición posterior para la lectura. Un flujo de entrada de audio puede admitir marcas. Cuando estableces una marca, se recuerda la posición actual para que puedas volver a ella más tarde.
La clase AudioSystem incluye muchos métodos que manipulan objetos AudioInputStream. Por ejemplo, los métodos te permiten:
Obtener un flujo de entrada de audio desde un archivo de audio externo, un flujo o una URL.
Escribir un archivo externo desde un flujo de entrada de audio.
Convertir un flujo de entrada de audio a un formato de audio diferente.
FilterInputStream: encapsula otro flujo de entrada y proporciona funcionalidad adicional. Ejemplo:
BufferedInputStream: lee bytes de un flujo de entrada y los almacena en un búfer interno.
DataInputStream: lee primitivos de datos Java del flujo de entrada.
PushbackInputStream: permite que los bytes leídos se devuelvan al flujo de entrada.
ObjectInputStream: lee objetos Java serializados del flujo de entrada.
ByteArrayOutputStream: implementa un flujo de salida en el que los datos se escriben en un array de bytes. El búfer crece automáticamente a medida que se escriben datos en él. Los datos se pueden recuperar usando toByteArray() y toString().
Cerrar a ByteArrayOutputStream no tiene ningún efecto. Los métodos de esta clase se pueden llamar después de que se haya cerrado la secuencia sin generar un archivo IOException.
FileOutputStream: flujo de salida para escribir datos en un archivo File o en un archivo FileDescriptor. El hecho de que un archivo esté disponible o pueda crearse depende de la plataforma subyacente. Algunas plataformas, en particular, permiten que un archivo sea abierto para escritura por solo uno FileOutputStream (u otro objeto de escritura de archivos) a la vez. En tales situaciones, los constructores de esta clase fallarán si el archivo involucrado ya está abierto.
FileOutputStream está destinado a escribir flujos de bytes sin formato, como datos de imágenes. Para escribir secuencias de caracteres debe usarse el orientado a carácter FileWriter.
ObjectOutputStream: escribe objetos Java serializados en un flujo de salida.
FilterOutputStream: encapsula otro flujo de salida y proporciona funcionalidad adicional. Ejemplo:
BufferedOutputStream: escribe bytes en un flujo de salida y los almacena en un búfer interno.
PrintStream: proporciona métodos para imprimir representaciones de datos primitivos y objetos en un flujo de salida. un ejemplo de uso es System.out.
CheckedOutputStream: calcula un valor de comprobación de suma de verificación (checksum)para los datos escritos en el flujo de salida. Se puede emplear para comprobar la integridad de los datos de salida.
CipherOutputStream: escribe datos cifrados en un flujo de salida. Está compuesto por un OutputStream y un objeto de tipo Cipher, para procesar los datos antes de escribirlos en el flujo de salida. Debe ser inicializado con un modo de cifrado y una clave.
DataOutputStream: escribe datos primitivos Java en el flujo de salida. Los datos se pueden recuperar usando DataInputStream.
DeflaterOutputStream: comprime los datos escritos en el flujo de salida. Tiene dos subclases: GZIPOutputStream y ZipOutputStream.
DigestOutputStream: calcula un resumen de mensaje de los datos escritos en el flujo de salida. Se puede emplear para comprobar la integridad de los datos de salida.
InflaterOutputStream: implanta un filtro de flujo de salida para descomprimir datos comprimidos en formato de compresión de “deflate”.
3. ObjectInputStream y ObjectOutputStream
ObjectInputStream: lee objetos Java serializados del flujo de entrada y los deserializa.
ObjectOutputStream: escribe objetos Java serializados en un flujo de salida.
Para emplear las clases ObjectInputStream, ObjectOutStream los objetos a leer (escribir deben implantar la interface: Serializable (dicha interface no tiene métodos para implantar)
Para escribir:
Object ob =new Object();
out.writeObject(ob); //out es un flujo de tipo ObjectOutputStreamout.writeObject(ob);
La serialización es el proceso de convertir un objeto en una secuencia de bytes que se pueden escribir en un flujo de salida y, posteriormente, reconstruir el objeto a partir de esos bytes. La deserialización es el proceso inverso: reconstruir un objeto a partir de una secuencia de bytes.
Ejercicio 2. Serialización
Crea una clase Persona con los atributos nombre y edad. Crea un programa que serialice y deserialice un objeto de tipo Persona.
Debe tener un menú con las siguientes opciones:
Añadir persona.
Mostrar personas.
Buscar persona (por número o por nombre, según consideres)
Crea una clase ColeccionPersonas que contenga una colección de objetos de tipo Persona. Implementa la interface Serializable y crea un programa que serialice y deserialice un objeto de tipo ColeccionPersonas.
4. Lectura desde URL
Para leer desde una URL, se puede emplear la clase URL y openStream():
import java.io.*;
publicclassLeerURL {
publicstaticvoidmain(String[] args) throws Exception {
// URL url = new URL("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/"); // Desaprobado.// Versión actualizada: URI uri =new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/");
URL url = uri.toURL();
try (InputStream is = url.openStream();
InputStreamReader isr =new InputStreamReader(is); // es un puente de bytes a caracteres.int c;
while ((c = isr.read()) !=-1) {
System.out.print((char) c);
}
}
// // Código equivalente con buffer:// try (InputStream is = url.openStream();// InputStreamReader isr = new InputStreamReader(is);// BufferedReader br = new BufferedReader(isr)) { // Lo veremos en el siguiente apartado.// String line;// while ((line = br.readLine()) != null) {// System.out.println(line);// }// } }
}
URI/URL
La clase URL tiene constructores desaprobados, se recomienda emplear URI para crear una URL:
URI uri =new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/");
URL url = uri.toURL();
url.openStream(); // Abreviatura de: url.openConnection().getInputStream(); // openConnection() devuelve un objeto de tipo URLConnection.
// Implantación de openStream() en la clase URL:publicfinal InputStream openStream() throws java.io.IOException {
return openConnection().getInputStream();
}
Los constructores de URL está desaprobada, se recomienda emplear URI:
URI uri =new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/");
URL url = uri.toURL();
URLConnection
El método openConnection() de URL devuelve un objeto de tipo URLConnection:
URI uri =new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/");
URL url = uri.toURL();
URLConnection urlConnection = url.openConnection();
urlConnection.getInputStream();
HttpURLConnection
Permite añadir elementos específicos de HTTP, como el tamaño del contenido, o el tipo de archivo:
URL url =new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/").toURL();
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); // Hereda de URLConnection httpConnection.getInputStream();
httpConnection.setRequestMethod("HEAD");
long tamanho = httpConnection.getContentLengthLong();
Ejercicio 4. Lectura de URL
Crea un programa que lea el contenido de una URL y lo muestre por pantalla.
Mejore el programa para que pida una URL y la guarde en un archivo en una carpeta seleccionada del disco mediante un JFileChooser.
¿Serías capaz de mostrar el tamaño del contenido de la URL? ¿Y que ponga la extensión adecuada al archivo?
Ayuda: Puedes emplear HttpURLConnection para obtener el tamaño del contenido y para obtener el Content-Type puedes emplear el método getContentType().
Ejercicio 5. Lectura de URL con HttpURLConnection
Amplía el ejercicio anterior para que emplee HttpURLConnection y muestre la información de la cabecera HTTP.
Los flujos de caracteres leen/escriben datos de texto y tienen nombres de clase que terminan en Reader o Writer.
Automáticamente, transforma caracteres Unicode (formato de Java) al conjunto de caracteres local.
Todas las clases descienden de Reader y Writer.
Hay muchas clases de flujos de carácter, como: FileReader (usa internamente FileInputStream), FileWriter (usa internamente FileOutpuStream). Todos los restantes flujos funcionan de igual modo, sólo difieren en la forma de construirlos.
Java almacena valores de caracteres utilizando convenciones Unicode. La E/S de flujos de caracteres traduce automáticamente este formato interno hacia y desde el conjunto de caracteres local. En locales occidentales, como el juego de caracteres Latin-1 o Windows-1252, el conjunto de caracteres local es generalmente un superconjunto de ASCII de 8 bits. En locales asiáticos, el conjunto de caracteres local es un conjunto de caracteres de doble byte.
En la E/S con flujos de caracteres, la entrada y salida realizada con clases de flujo se traduce automáticamente hacia y desde el conjunto de caracteres local. Un programa que utiliza flujos de caracteres en lugar de flujos de bytes se adapta automáticamente al conjunto de caracteres local y está listo para la internacionalización, todo sin esfuerzo adicional por parte del programador.
Si la internacionalización no es prioritario, puedes usar las clases de flujos de caracteres sin prestar mucha atención a los problemas de conjunto de caracteres. Más tarde, si la internacionalización se convierte en una prioridad, tu programa puede adaptarse sin una recodificación extensa (existen constructores y métodos que recogen el juego de caracteres).
1. Reader y Writer
Todas las clases de flujos de caracteres heredan de la clase abstracta Reader y la clase abstracta Writer. Al igual que con los flujos de bytes, existen clases de flujos de caracteres que se especializan en la E/S de archivos: FileReader y FileWriter. El ejemplo CopiarCaracteres ilustra estas clases.
CopiarCaracteres es muy similar a CopiaArchivos. La diferencia más importante es que CopiarCaracteres ==utiliza FileReader y FileWriter para entrada y salida en lugar de FileInputStream y FileOutputStream==. Observa que tanto CopiaArchivos como CopiarCaracteres emplean una variable int para leer y escribir. Sin embargo, en CopiarCaracteres, la variable int contiene un valor de carácter en sus últimos 16 bits; en CopiaArchivos, la variable int contiene un valor de byte en sus últimos 8 bits.
Con try-with-resources, el código es más limpio y más fácil de leer. FileReader y FileWriter se cierran automáticamente cuando el bloque try-with-resources se completa:
Los flujos de caracteres suelen ser “envoltorios” para flujos de bytes. El flujo de caracteres utiliza el flujo de bytes para realizar la E/S física, mientras que el flujo de caracteres maneja la traducción entre caracteres y bytes. FileReader, por ejemplo, utiliza FileInputStream, mientras que FileWriter utiliza FileOutputStream.
InputStreamReader y OutputStreamWriter
Son flujos de caracteres que leen y escriben bytes, respectivamente, son flujos de “puente” byte-a-carácter de propósito general: InputStreamReader y OutputStreamWriter.
Se emplean para crear flujos de caracteres cuando no haya clases de flujo de caracteres preempaquetadas que cumplan con las necesidades. Por ejemplo, para crear flujos de caracteres a partir de los flujos de bytes proporcionados por las clases de Socket, como se muestra en el siguiente ejemplo:
import java.io.*;
import java.net.*;
publicclassClienteEcho {
publicstaticvoidmain(String[] args) throws IOException {
if (args.length!= 2) {
System.err.println("Uso: java ClienteEcho <nombre host> <número puerto>");
System.exit(1);
}
String nombreHost = args[0];
int numeroPuerto = Integer.parseInt(args[1]);
try (Socket echoSocket =new Socket(nombreHost, numeroPuerto); // Socket es un flujo de bytes PrintWriter out =new PrintWriter(echoSocket.getOutputStream(), true); // PrintWriter es un flujo de caracteres que envía datos a un flujo de bytes. true para autoflush. Escribirá en el flujo de bytes cada vez que se llame a println BufferedReader in =new BufferedReader(new InputStreamReader(echoSocket.getInputStream())); // InputStreamReader es un puente byte a carácter, leemos bytes del flujo de bytes y los convertimos a caracteres BufferedReader stdIn =new BufferedReader(new InputStreamReader(System.in)) // InputStreamReader es un puente byte a carácter ) {
String entradaUsuario;
while ((entradaUsuario = stdIn.readLine()) !=null) {
out.println(entradaUsuario); // envía la entrada del usuario al servidor System.out.println("echo: "+ in.readLine());
}
} catch (UnknownHostException e) {
System.err.println("Host desconocido "+ nombreHost);
System.exit(1);
} catch (IOException e) {
System.err.println("NO ha sido posible establecer la conexión con "+ nombreHost);
System.exit(1);
}
}
}
La E/S de caracteres suele ocurrir en unidades más grandes que los caracteres individuales. Una unidad común es la línea: una cadena de caracteres con un terminador de línea al final.
Terminadores de línea
Un terminador de línea puede ser una secuencia de retorno de carro/avance de línea ("\r\n"), un solo retorno de carro ("\r"), o un solo avance de línea ("\n"). Admitir todos los terminadores de línea posibles permite a los programas leer archivos de texto creados en cualquiera de los sistemas operativos ampliamente utilizados.
En Windows, el terminador de línea es “\r\n”. En Unix, el terminador de línea es “\n”. En Macintosh, el terminador de línea es “\r”.
Modifiquemos el ejemplo CopiarCaracteres para usar E/S orientada a líneas. Para hacer esto, tenemos que usar dos clases con buffer o memoria internmedia (que guarda los caracteres de toda la línea, o más), BufferedReader y PrintWriter. Veresmos estas clases con mayor profundidad en E/S en el siguiente apartado.
El ejemplo CopiarCaracteresinvoca BufferedReader.readLine y PrintWriter.println para realizar la entrada y salida una línea a la vez.
Invocar readLine devuelve una línea de texto con la línea. CopiaLineas genera cada línea usando println, que añade el terminador de línea para el sistema operativo actual. Esto puede que no sea el mismo terminador de línea que se usó en el archivo de entrada.
Hay muchas maneras de estructurar la entrada y salida de texto más allá de caracteres y líneas.
3. Diagrama de clases de Reader Java:
La API a menudo incluye clases similares tanto para flujos de bytes como para flujos de caracteres, como FileInputStream y FileReader. La diferencia entre las dos clases se basa en cómo se leen o escriben los bytes en el flujo.
Es importante recordar que, aunque los flujos de caracteres no contienen la palabra “Stream” en su nombre de clase, siguen siendo flujos de E/S. El uso de “Reader/Writer” en el nombre es simplemente para distinguirlas de los flujos de bytes.
Los flujos de bytes se utilizan principalmente para trabajar con datos binarios, como una imagen o un archivo ejecutable, mientras que los flujos de caracteres se utilizan para trabajar con archivos de texto. Dado que las clases de flujos de bytes pueden escribir todo tipo de datos binarios, incluidas cadenas, se deduce que las clases de flujos de caracteres no son estrictamente necesarias. Sin embargo, existen ventajas en usar las clases de flujos de caracteres, ya que se centran específicamente en la gestión de datos de caracteres y cadenas. Por ejemplo, puedes emplear una clase Writer para escribir un valor de cadena en un archivo sin necesidad de preocuparte por la codificación de caracteres subyacente del archivo.
La codificación de caracteres determina cómo se codifican y almacenan los caracteres en bytes en un flujo y cómo se leen posteriormente o decodifican como caracteres. Aunque esto puede parecer sencillo, Java admite una amplia variedad de codificaciones de caracteres, desde aquellas que pueden utilizar un byte para caracteres latinos, como UTF-8 y ASCII, hasta aquellas que utilizan dos o más bytes por carácter, como UTF-16. No es necesario entrar en detalle sobre las codificaciones de caracteres, pero debes estar familiarizado con sus nombres si te encuentras con ellos algún día y saber por dónde van los tiros ;-).
Flujo de caracteres para texto
En cuanto a la codificación de caracteres, simplemente recuerda que usar un flujo de caracteres es mejor para trabajar con datos de texto que un flujo de bytes. Las clases de flujos de caracteres se crearon por conveniencia, y debes aprovecharlas cuando sea posible.
Otra forma de familiarizarse con la API java.io es dividir los flujos en flujos de bajo nivel y flujos de alto nivel (con buffer o memoria intermedia).
1.1. Flujos de bajo nivel (sin buffer)
Un flujo de bajo nivel (sin buffer) se conecta directamente a la fuente de datos, como un archivo, un array o un String. Los flujos de bajo nivel procesan los datos o recursos en bruto y se acceden de manera directa y sin filtrar.
Por ejemplo, una FileInputStream es una clase que lee datos de archivos de un byte a la vez.
En los flujos sin buffer cada petición de lectura/escritura se envía directamente al sistema E/S: puede ser ineficiente (acceso a disco, actividad de red,…)
1.2. Flujos de alto nivel (con buffer)
Por otro lado, un flujo de alto nivel se construye sobre un flujo mediante el encapsulamiento. La encapsulación es el proceso mediante el cual una instancia se pasa al constructor de otra clase, y las operaciones en la instancia resultante se filtran y aplican a la instancia original.
Por ejemplo, echa un vistazo a los objetos FileReader y BufferedReader en el siguiente código de ejemplo:
En este ejemplo, FileReader es el flujo de bajo nivel para la lectura, mientras que BufferedReader es el flujo de alto nivel que toma un FileReader como entrada. Muchas operaciones en el flujo de alto nivel pasan como operaciones a el flujo de bajo nivel subyacente, como read() o close(). Otras operaciones anulan o agregan nueva funcionalidad a los métodos de el flujo de bajo nivel.
Un flujo de buffer puede agregar nuevos métodos, como readLine(), así como mejoras de rendimiento para leer y filtrar los datos de bajo nivel.
Los flujos de alto nivel pueden tomar otros flujos de alto nivel como entrada. Por ejemplo, aunque el siguiente código pueda parecer un poco extraño al principio, el estilo de encapsular un flujo es bastante común en la práctica:
En este ejemplo, FileInputStream es el flujo de bajo nivel que interactúa directamente con el archivo, la cual está envuelta por BufferedInputStream de alto nivel para mejorar el rendimiento. Finalmente, el objeto completo está envuelto por ObjectInputStream, de alto nivel, que nos permite interpretar los datos como un objeto Java.
Las únicas clases de flujos de bajo nivel con las que debes estar familiarizado son las que operan en archivos. El resto de las clases de flujos no abstractas son todas flujos de alto nivel.
Utiliza flujos con búfer al trabajar con archivos
Como se comentó brevemente, las clases con “Buffered” leen o escriben datos en bloques en lugar de un solo byte o carácter a la vez. La mejora de rendimiento al utilizar una clase con búfer para acceder a un flujo de bajo nivel de archivos no se puede exagerar. A menos que estés haciendo algo muy especializado en tu aplicación, siempre debes envolver un flujo de archivo con una clase con búfer en la práctica.
Una de las razones por las que los flujos con búfer tienden a funcionar tan bien en la práctica es que muchos sistemas de archivos están optimizados para el acceso secuencial al disco. Cuantos más bytes secuenciales leas a la vez, menos viajes de ida y vuelta entre el proceso Java y el sistema de archivos, lo que mejora el acceso de tu aplicación. Por ejemplo, acceder a 1,600 bytes secuenciales es mucho más rápido que acceder a 1,600 bytes dispersos por el disco duro.
2. Clases base para flujos: InputStream, OutputStream, Reader y Writer
La biblioteca java.io define cuatro clases abstractas que son las clases base de todas las clases de flujos definidas en la API:
InputStream
OutputStream
Reader
Writer
Frecuentemente, los constructores de flujos de alto nivel toman una referencia de la clase abstracta. Por ejemplo, BufferedWriter toma un objeto Writer como entrada, lo que le permite tomar cualquier subclase de Writer.
Es un error común para iniciados mezclar y combinar clases de flujos que no son compatibles entre sí. Por ejemplo, echa un vistazo a cada uno de los siguientes ejemplos y ve si puedes determinar por qué no se compilan:
new BufferedInputStream(new FileReader("z.txt")); // NO COMPILA por mezclar clases de Reader con clases de InputStreamnew BufferedWriter(new FileOutputStream("z.txt")); // NO COMPILA por mezclar clases de Writer con clases de OutputStreamnew ObjectInputStream(new FileOutputStream("z.txt")); // NO COMPILA por mezclar clases de InputStream con clases de OutputStreamnew BufferedInputStream(new InputStream()); // NO COMPILA porque InputStream es una clase abstracta
Los primeros dos ejemplos no se compilan porque mezclan clases de Reader/Writer con clases de InputStream/OutputStream, respectivamente. El tercer ejemplo no se compila porque estamos mezclando una OutputStream con una InputStream. Aunque es posible leer datos de una InputStream y escribirlos en una OutputStream, envolver el flujo no es la forma de hacerlo.
Como veremos más adelante, los datos deben copiarse, a menudo de manera iterativa. Finalmente, el último ejemplo no se compila porque InputStream es una clase abstracta y, por lo tanto, no puedes crear una instancia de ella.
2.1. Identificación de clases de E/S con flujos
Presta atención al nombre de la clase de E/S, ya que descifrarlo a menudo te proporciona pistas de contexto sobre lo que hace la clase. Por ejemplo, sin necesidad de buscarlo, debería estar claro que FileReader es una clase que lee datos de un archivo como caracteres o cadenas. Además, ObjectOutputStream parece una clase que escribe datos de objeto en un flujo de bytes.
Revisión de las Propiedades de los Nombres de Clase de java.io
Una clase con las palabras "InputStream" u “OutputStream” en su nombre se utiliza para leer o escribir datos binarios (o de bytes), respectivamente.
Una clase con las palabras "Reader" o “Writer” en su nombre se utiliza para leer o escribir datos de caracteres (o cadenas), respectivamente.
La mayoría, pero no todas, las clases de entrada tienen una clase de salida correspondiente (FileInputStream y FileOutputStream, por ejemplo)
Un flujo de bajo nivel se conecta directamente a la fuente de datos (FileInputStream y FileOutputStream, por ejemplo):
FileReader in =new FileReader("unaVacaLoca.mp3");
Un flujo de buffer se construye sobre otro flujo de bajo mediante encapsulación (dentro de un buffer):
BufferedReader in =new BufferedReader(new FileReader("chocolateCaramelo.mp3"));
Una clase con "Buffered" en su nombre lee o escribe datos en grupos de bytes o caracteres de una memoria intermedia o buffer y, a menudo, mejora el rendimiento en sistemas de archivos secuenciales.
Con algunas excepciones, sólo envuelves un flujo con otro flujo si comparten el mismo padre abstracto (FileReader puede ser encapsulado en un BufferedReader, por ejemplo), salvo clases que pasan flujos de bytes (InputStream) en caracteres (Reader), por ejemplo: InputStreamReader
3. Tabla resumen de clases de flujos de E/S
Tabla 1 y Tabla 2 se muestran las clases base abstractas de flujos y las clases concretas de flujos de E/S que debes conocer. Ten en cuenta que la mayoría de la información sobre cada flujo, como si es de entrada o salida o si accede a datos mediante bytes o caracteres, se puede deducir solo por el nombre.
Tabla 1Las clases base abstractas de flujos de E/S de java.io:
Clase
Descripción
InputStream
Clase abstracta para todas los flujos de entrada de bytes
OutputStream
Clase abstracta para todas los flujos de salida de bytes
Reader
Clase abstracta para todas los flujos de entrada de caracteres
Writer
Clase abstracta para todas los flujos de salida de caracteres
Tabla 2Clases implemementadas de flujos de E/S de java.io que debes conocer:
Clase
Bajo/Alto Nivel
Descripción
FileInputStream
Bajo
Lee datos de archivos como bytes
FileOutputStream
Bajo
Escribe datos de archivos como bytes
FileReader
Bajo
Lee datos de archivos como caracteres
FileWriter
Bajo
Escribe datos de archivos como caracteres
BufferedInputStream
Alto
Lee datos de bytes de un flujo de entrada existente de manera bufferizada, lo que mejora la eficiencia y el rendimiento
BufferedOutputStream
Alto
Escribe datos de bytes en un flujo de salida existente de manera bufferizada, lo que mejora la eficiencia y el rendimiento
BufferedReader
Alto
Lee datos de caracteres de un objeto Reader existente de manera bufferizada, lo que mejora la eficiencia y el rendimiento
BufferedWriter
Alto
Escribe datos de caracteres en un objeto Writer existente de manera bufferizada, lo que mejora la eficiencia y elrendimiento
ObjectInputStream
Alto
Deserializa tipos de datos primitivos de Java y gráficos de objetos de Java a partir de un flujo de entrada existente
ObjectOutputStream
Alto
Serializa tipos de datos primitivos de Java y gráficos de objetos de Java en un flujo de salida existente
PrintStream
Alto
Escriberepresentaciones formateadas de objetos Java en un flujo binario
PrintWriter
Alto
Escribe representaciones formateadas de objetos Java en un flujo de caracteres
Aunque existen muchas clases de flujos, muchas de ellas comparten las mismas operaciones. En esta sección, revisaremos los métodos comunes entre varias clases de flujos. En la siguiente sección, cubriremos clases de flujos específicas.
1.1. Lectura y escritura de Datos
Los flujos de E/S se tratan de leer y escribir datos, por lo que no debería sorprendernos que los métodos más importantes sean read() y write(). Tanto InputStream como Reader declaran el siguiente método para leer datos de bytes de un flujo:
// InputStream y Readerpublicintread() throws IOException
Del mismo modo, OutputStream y Writer definen el siguiente método para escribir un byte en el flujo:
// OutputStream y Writerpublicvoidwrite(int b) throws IOException
Espera un momento. Dijimos que estamos leyendo y escribiendo bytes, ¿entonces por qué los métodos usan int en lugar de byte? Recuerda, el tipo de dato byte tiene un rango de 256 caracteres. Se necesitaba un valor adicional para indicar el final de un flujo. Los autores de Java decidieron usar un tipo de dato más grande, int, para que valores especiales como -1 indiquen el final de un flujo. Las clases de flujos de salida también utilizan int para ser coherentes con las clases de flujos de entrada.
// Ejemplo de métodos copyStream() que leen desde un InputStream o Reader// y escriben en un OutputStream o Writer, respectivamente. En ambos ejemplos,// -1 se usa para indicar el final del flujo.voidcopyStream(InputStream in, OutputStream out) throws IOException {
int b;
while ((b = in.read()) !=-1) {
out.write(b);
}
}
voidcopyStream(Reader in, Writer out) throws IOException {
int b;
while ((b = in.read()) !=-1) {
out.write(b);
}
}
Las clases de flujos de bytes también incluyen métodos sobrecargados para leer y escribir múltiples bytes a la vez.
// InputStreampublicintread(byte[] b) throws IOException
publicintread(byte[] b, int offset, int length) throws IOException
// OutputStreampublicvoidwrite(byte[] b) throws IOException
publicvoidwrite(byte[] b, int offset, int length) throws IOException
Los valores de offset y length se aplican al array en sí. Por ejemplo, un offset de 5 y una longitud de 3 indican que el flujo debería leer hasta 3 bytes de datos y colocarlos en el array comenzando desde la posición 5.
Existen métodos equivalentes para las clases de flujos de caracteres que usan char en lugar de byte.
// Readerpublicintread(char[] c) throws IOException
publicintread(char[] c, int offset, int length) throws IOException
// Writerpublicvoidwrite(char[] c) throws IOException
publicvoidwrite(char[] c, int offset, int length) throws IOException
1.2. Cierre de flujos
Todos los flujos de E/S incluyen un método para liberar cualquier recurso dentro del flujo cuando ya no se necesita.
// Todas las clases de flujos de E/Spublicvoidclose() throws IOException
Dado que los flujos se consideran recursos, es fundamental que todos los flujos de E/S se cierren después de su uso, para evitar posibles fugas de recursos.
Dado que todos los flujos de E/S implementan la interfaz Closeable, la mejor manera de hacerlo es con una declaración try-with-resources.
En muchos sistemas de archivos, no cerrar un archivo correctamente podría dejarlo bloqueado por el sistema operativo, impidiendo que otros procesos lo lean o escriban hasta que el programa se termine. EN la medida de lo posible, cerraremos los recursos del flujo usando la sintaxis de try-with-resources, ya que esta es la forma preferida de cerrar recursos en Java. También utilizaremos var para acortar las declaraciones , ya que estas declaraciones pueden volverse bastante largas (en el aula suelo poner el nombre de clase para poner el tipo concreto y que lo conozcáis, pero es mejor hacerlo con var).
¿Y si necesitas pasar un flujo a un método? Eso está bien, pero el flujo debe cerrarse en el método que lo creó.
En este ejemplo, el flujo se crea y se cierra en el método readFile(), mientras que printData() procesa su contenido.
1.3. Cierre de flujos envueltos en otro flujo (con buffer)
Cuando trabajas con un flujo envuelto (con buffer), solo necesitas usar close() en el objeto superior. Al hacerlo, se cerrarán los flujos subyacentes.
El siguiente ejemplo es válido y resultará en tres llamadas separadas a close(), pero es innecesario:
try (var fis =new FileOutputStream("zoo-banner.txt");
// Innecesariovar bis =new BufferedOutputStream(fis);
var ois =new ObjectOutputStream(bis)) {
ois.writeObject("Hola");
}
En cambio, podemos confiar en que ObjectOutputStream cierre BufferedOutputStream y FileOutputStream. Lo siguiente llamará solo a un método close() en lugar de tres:
try (var ois =new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream("zoo-banner.txt")))) {
ois.writeObject("Hola");
}
1.4. Manipulación de flujos de entrada: Mark, Reset y Skip
Todas las clases de flujos de entrada incluyen los siguientes métodos para manipular el orden en el que se leen los datos de un flujo:
Los métodos mark() y reset() devuelven un flujo a una posición anterior.
Antes de llamar a cualquiera de estos métodos, debes llamar al método markSupported(), que devuelve true solo si mark() es compatible.
El método skip() es bastante simple; básicamente, lee datos del flujo y descarta el contenido.
mark() y reset()
Supongamos que tenemos una instancia de InputStream cuyos próximos valores son “LEON”. Considera el siguiente fragmento de código:
publicvoidreadData(InputStream is) throws IOException {
System.out.print((char) is.read()); // Lif (is.markSupported()) {
is.mark(100); // Marca hasta 100 bytes System.out.print((char) is.read()); // E System.out.print((char) is.read()); // O is.reset(); // Restablece el flujo a la posición antes de E }
System.out.print((char) is.read()); // E System.out.print((char) is.read()); // O System.out.print((char) is.read()); // N}
El fragmento de código imprimirá “LEOEON” si mark() es compatible, y “LEON” en caso contrario. Es una buena práctica organizar las operaciones read() de modo que el flujo termine en la misma posición, independientemente de si mark() es compatible o no.
¿Y qué hay del valor 100 que pasamos al método mark()? Este valor se llama readLimit. Le indica al flujo que esperamos llamar a reset() después de leer como máximo 100 bytes. Si el programa llama a reset() después de leer más de 100 bytes al llamar a mark(100), entonces podría lanzar una excepción, dependiendo de la clase de flujo.
skip()
Supongamos que tenemos una instancia de InputStream cuyos próximos valores son “TIGRES”. Considera el siguiente fragmento de código:
System.out.print((char) is.read()); // Tis.skip(2); // Salta I y Gis.read(); // Lee R pero no lo muestraSystem.out.print((char) is.read()); // ESystem.out.print((char) is.read()); // S
Este código imprimirá “TES” en tiempo de ejecución. Hemos saltado dos caracteres, I y G. También leímos R pero no lo almacenamos en ninguna parte, por lo que se comporta como si hubiéramos llamado a skip(1).
El valor devuelto por skip() nos indica cuántos valores se omitieron realmente . Por ejemplo, si estamos cerca del final del flujo y llamamos a skip(1000), el valor de retorno podría ser 20, lo que indica que se alcanzó el final del flujo después de omitir 20 valores. Usar el valor devuelto por skip() es importante si necesitas llevar un registro de dónde estás en un flujo y cuántos bytes se han procesado.
1.5. Flushing de flujos de salida (Output Streams)
Cuando se escribe datos en un flujo de salida, el sistema operativo subyacente no garantiza que los datos se escriban inmediatamente en el sistema de archivos. En muchos sistemas operativos, los datos pueden almacenarse en la memoria, y la escritura se produce solo después de que se llena una caché temporal o después de un cierto período de tiempo.
Si los datos se almacenan en la memoria y la aplicación termina de manera inesperada, los datos se perderán, ya que nunca se escribieron en el sistema de archivos. Para abordar esto, todas las clases de flujos de salida proporcionan un método flush(), que solicita que todos los datos acumulados se escriban de inmediato en el disco.
// OutputStream y Writerpublicvoidflush() throws IOException
En el siguiente ejemplo, se escriben 1000 caracteres en un flujo de archivo. Las llamadas a flush() aseguran que los datos se envíen al disco duro al menos una vez cada 100 caracteres. La JVM o el sistema operativo son libres de enviar los datos con más frecuencia.
try (var fos =new FileOutputStream(fileName)) {
for (int i = 0; i < 1000; i++) {
fos.write('a');
if (i % 100 == 0) {
fos.flush();
}
}
}
El método flush() ayuda a reducir la cantidad de datos perdidos si la aplicación termina de manera inesperada. Sin embargo, no es gratuito. Cada vez que se usa, puede causar un retraso perceptible en la aplicación, especialmente para archivos grandes. A menos que los datos que estés escribiendo sean extremadamente críticos, el método flush() solo debe usarse de manera intermitente. Por ejemplo, no es necesario llamarlo después de cada escritura.
Tampoco es necesario llamar al método flush() cuando hayas terminado de escribir datos, ya que ==el método close() lo hará automáticamente=0.
2. Resumen de métodos más comunes de flujos de E/S
La Tabla 3 revisa los métodos comunes de flujos que debes conocer para este apartado.
Para los métodos read() y write() que toman arrays primitivos, el tipo de parámetro del método depende del tipo de flujo. Los flujos de bytes que terminan en InputStream/OutputStream utilizan byte[], mientras que los flujos de caracteres que terminan en Reader/Writer utilizan char[].
Tabla 3: Métodos de flujos de E/S más importantes
Flujo
Nombre del Método
Descripción
Todos los flujos
void close()
Cierra el flujo y libera los recursos
Todos los flujos de entrada
int read()
Lee un solo byte o devuelve -1 si no hay bytes disponibles
InputStream
int read(byte[] b)
Lee valores en un búfer. Devuelve el número de bytes leídos
Reader
int read(char[] c)
Lee valores en un búfer. Devuelve el número de bytes leídos
InputStream
int read(byte[] b, int offset, int length)
Lee hasta length valores en un búfer, comenzando desde la posición offset. Devuelve el número de bytes leídos
Reader
int read(char[] c, int offset, int length)
ee hasta length valores en un búfer, comenzando desde la posición offset. Devuelve el número de bytes leídos
Todos los flujos de salida
void write(int)
Escribe un solo byte
OutputStream
void write (byte[] b)
Escribe un array de valores en el flujo
Writer
void write(char[] c)
Escribe un array de valores en el flujo
OutpuStream
void write(byte[] c, int offset, int length)
Escribe length valores del array en un flujo, empezando desde el índice offset
Writer
void write(char[] c, int offset, int length)
Escribe length valores del array en un flujo, empezando desde el índice offset-
Todos los flujos de entrada
boolean markSupported()
Devuelve true si la clase de flujo admite mark()
Todos los flujos de entrada
void mark(int readLimit)
Marca la posición actual en el flujo
Todos los flujos de entrada
void reset()
Intenta restablecer el flujo a la posición marcada
Todos los flujos de entrada
long skip(long n)
Lee y descarta un número especificado de caracteres
Boletín 01. Ejercicos con la clase File Y RandomAccessFile
Recuerda
Para realizar los ejercicios de este boletín, debes crear un nuevo proyecto en tu IDE preferido y añadir las clases que se indican en cada ejercicio.
También debes consultar la documentación oficial de la clase File para conocer los métodos que puedes utilizar, así como el apartado: de “Ventanas de entrada de datos, mensajes y archivos” de la unidad de “Refuerzo y ayudas complementarias”, apartado “Java General”
Ejercicio 1. Creación y lectura de archivos con File
Debes trabajar únicamente con métodos de la clase File.
Realiza los siguientes pasos:
Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
Escribe un programa que cree un objeto File para el archivo prueba.txt y compruebe si el archivo existe.
Si el archivo existe, muestra la ruta absoluta, nombre del archivo, tamaño, última modificación y si es un directorio.
Si el archivo no existe, muestra un mensaje que lo indique y crea uno temporal.
Ejercicio 2. Mostrar el contenido de un directorio
Debes trabajar únicamente con métodos de la clase File.
El programa abre una ventana para la selección de un directorio (hazlo también desde teclado si recoge un parámetro) y usando el método listFiles() de la clase File, muestra el contenido de ese directorio, indicando el tamaño de los archivos y si es un directorio o no.
Además, muestra el tamaño total de los archivos y directorios.
Muestra en una ventana emergente el resultado y por consola.
A continuación puedes ver algunas soluciones parciales del ejercicio 2. Completa el ejercicio de acuerdo a las indicaciones.
Como en todos los ejercicios anteriores, debes trabajar únicamente con métodos de la clase File.
Escribe un programa en Java que funcione como un gestor básico de archivos y directorios. El programa debe permitir al usuario realizar las siguientes operaciones:
Crear un directorio, empleando la clase JFileChooser para seleccionar la ruta donde se creará.
Listar todos los archivos y subdirectorios de un directorio de forma recursiva.
Eliminar un archivo o directorio. Si es un directorio, eliminar todo su contenido de forma recursiva.
Mover o renombrar archivos y directorios.
El programa debe ofrecer un menú para que el usuario elija la operación que desea realizar. La selección de directorios o archivos debe realizarse con la clase JFileChooser.
Ejercicio 4. Escritura y lectura de archivos con RandomAccessFile
Escribe un programa que escriba y lea datos en un archivo usando la clase RandomAccessFile.
Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
Escribe un programa que cree un objeto RandomAccessFile para el archivo prueba.txt y escriba un mensaje.
Lee el mensaje y muéstralo por consola.
Ejercicio 5. Escritura y lectura de archivos con RandomAccessFile
Escribe un programa que utilice la clase RandomAccessFile para escribir en un archivo los números del 1 al 10 y luego los lea desde el archivo. Muestra los números leídos en la consola.
Solución al ejercicio 5
import java.io.IOException;
import java.io.RandomAccessFile;
publicclassRandomAccessFileDemo {
publicstaticvoidmain(String[] args) {
try {
RandomAccessFile raf =new RandomAccessFile("prueba.txt", "rw");
for (int i = 1; i <= 10; i++) {
raf.writeInt(i);
}
raf.seek(0);
for (int i = 1; i <= 10; i++) {
System.out.println(raf.readInt());
}
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Ejercicio 6. Modificación de Contenido en un Archivo Binario con RandomAccessFile
Escribe un programa en Java que haga lo siguiente:
Escriba 10 enteros en un archivo llamado “datos.bin”.
Permita al usuario modificar el tercer número almacenado en el archivo por otro número.
Muestra los números antes y después de la modificación en la consola.
Solución al ejercicio 6
import java.io.RandomAccessFile;
import java.io.IOException;
import java.util.Scanner;
publicclassEjercicio3 {
publicstaticvoidmain(String[] args) {
try (RandomAccessFile raf =new RandomAccessFile("datos.bin", "rw")) {
// Escribir 10 enteros en el archivofor (int i = 1; i <= 10; i++) {
raf.writeInt(i);
}
// Leer los números antes de la modificación System.out.println("Números antes de la modificación:");
raf.seek(0);
for (int i = 0; i < 10; i++) {
System.out.println(raf.readInt());
}
// Solicitar al usuario un nuevo número para el tercer número Scanner sc =new Scanner(System.in);
System.out.print("Introduce un nuevo número para reemplazar el tercer número: ");
int nuevoNumero = sc.nextInt();
// Modificar el tercer número (posición 2 en base 0, cada entero ocupa 4 bytes) raf.seek(2 * 4);
raf.writeInt(nuevoNumero);
// Leer los números después de la modificación System.out.println("Números después de la modificación:");
raf.seek(0);
for (int i = 0; i < 10; i++) {
System.out.println(raf.readInt());
}
} catch (IOException e) {
System.out.println("Ocurrió un error de entrada/salida.");
e.printStackTrace();
}
}
}
Ejercicio 7. Escritura y lectura de archivos con RandomAccessFile
Escribe un programa que escriba y lea datos en un archivo usando la clase RandomAccessFile. El programa debe hacer lo siguiente:
Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
Escribe un programa que cree un objeto RandomAccessFile para el archivo prueba.txt y escriba un mensaje.
Lee el mensaje y muéstralo por consola.
Boletín 02. Ejercicios con flujos I/O
Ejercicio 1. Copia de archivos I/O
Se debe realizar un programa para copiar archivos. El programa debe recoger el nombre del archivo origen y destino. Se existe debe solicitar confirmación sobrescribir.
Úsese I/O con buffer y métodos estáticos (tenga en cuenta que los archivos pueden ser binarios).
a) Para la lectura desde teclado puede emplearse la clase Scanner.
b) Realiza el mismo ejercicio, pero empleando entradas desde ventana con JFileChooser`` y mensajes de error en JOptionPane, si los hay.
c) Realiza un programa que lea con un JOptionPane pida una URL y para posteriormente abrir un JFileChooser para guardarlo en el disco local.
Ayuda: para abrir un flujo de entrada a una URL puede hacerse con el método openStream() de URL. Ten en cuenta que puede lanzar excepciones:
InputStream in =new URL(FILE_URL).openStream();
d) Mejora el aparado a) para que la lectura de los datos lo haga en bloques
(buffer) y no byte a byte.
Ejercicio 2. Serialización
Crea una clase Persona con los atributos nombre y edad. Genera un programa que serialice y deserialice un objeto de tipo Persona.
Debe tener un menú con las siguientes opciones:
Añadir persona.
Mostrar personas.
Buscar persona (por número o por nombre, según consideres)
Crea una clase ColeccionPersonas que contenga una colección de objetos de tipo Persona. Implementa la interface Serializable y crea un programa que serialice y deserialice un objeto de tipo ColeccionPersonas.
Ejercicio 4. Lectura de URL
Crea un programa que lea el contenido de una URL y lo muestre por pantalla.
Mejore el programa para que pida una URL y la guarde en un archivo en una carpeta seleccionada del disco mediante un JFileChooser.
¿Serías capaz de mostrar el tamaño del contenido de la URL? ¿Y que ponga la extensión adecuada al archivo?
Ayuda: Puedes emplear HttpURLConnection para obtener el tamaño del contenido y para obtener el Content-Type puedes emplear el método getContentType().
Ejercicio 5. Lectura de URL con HttpURLConnection
Amplía el ejercicio anterior para que emplee HttpURLConnection y muestre la información de la cabecera HTTP.
Ejercicio 6. Estadísticas de un archivo
Realice un programa que recoja el nombre de un fichero y muestre una estadística de la ruta, número de líneas, número de espacios, número de letras, fecha última modificación, longitud del fichero, …
Defina una clase EstatisticaFile con atributos: letras, linhas, espacios, archivo (tipo File).
Métodos para obtener cada uno de los atributos, existe(), ultimaModificacion(), getRuta().
El constructor recoge el nombre del archivo.
Ejercicio 7. Estadísticas de un archivo con RandomAccessFile
Realice un programa que recoja el nombre de un fichero y muestre una estadística de la ruta, número de líneas, número de espacios, número de letras, fecha última modificación, longitud del fichero, …
Defina una clase EstatisticaFile con atributos: letras, linhas, espacios, archivo (tipo File).
Métodos para obtener cada uno de los atributos, existe(), ultimaModificacion(), getRuta().
El constructor recoge el nombre del archivo.
Ejercicio 8. Editor de texto
Haz un programa que recoja el nombre de un fichero y muestre su contenido si existe o cree un nuevo en el que puedas escribir si no existe. Ejemplo: java Editor proba.txt
Para tal fin, además del programa, Editor.java, crea la clase Documento con las siguientes características:
Propiedades: arquivo (de tipo File)
Constructores: recoge el nombre del archivo y crea el objeto archivo. Otro que recoja un Objeto de tipo File.
Métodos:
exists(): devuelve verdadero cuando el fichero no es nulo y existe.
readFile(): devuelve una cadena con el contenido del archivo, si existe, obviamente. Emplea StringBuilder.
readFileNIO(): igual al anterior, pero empleado Path y el método readString de Files.
writeFromString(…): recoge una cadena y la escribe en fichero, al final, empleando BufferedWriter.
writeFromStringPrintWriter(…): recoge una cadena y la escribe al final, empleando PrintWriter.
writeFromInputStream(): rue recoge un flujo de tipo InputStream (para, por ejemplo, System.in) y escribe lo recogido por el flujo en el fichero.
writeFromKeyword(): escribe en el archivo lo que se escriba en el teclado.
. getFile(): devuelve el objeto archivo.
toString(): devuelve la ruta absoluta/canónica al archivo.
AppEditor.java recoge el nombre por línea de órdenes. Si existe, muestra el contenido (llama al método readFile()) si no existe pide que introduzcas por teclado. Para acabar de introducir datos debe escribir una línea que sólo contiene un “.”.
Ejercicio 9. Lectura de teclado
Realiza una clase de utilidad Teclado con métodos y atributos estáticos para leer desde teclado, que tenga un atributo estático privado LECTOR de tipo BufferedReader (lector de caracteres con buffer que permite leer línea la línea). La clase debe ter los siguientes métodos: lerString, lerChar, lerInt, lerLong, lerBoolean, lerFloat, lerDouble, lerByte, lerShort, para cada tipo de dato básico.
Haz un pequeño programa que haga uso esta clase.
Ayuda: emplead el atributo estático System.in (de tipo java.io.InputStream), así como la clase correspondiente que permita pasar un flujo de tipo Byte a un flujo de tipo Carácter.
Como sabéis, Java ya incorpora clases para facilitar la lectura desde teclado: java.io.Console (java 1.6 y sup.) e java.util.Scanner (java 1.5 e sup.), entre otras, como BufferedReader (java 1.1 y sup.).
Ejercicio 10. Gestión de equipos de fútbol
Haga un programa de gestión de la clasificación de la liga de fútbol. Declare una clase Equipo con los atributos mínimos necesarios: nome, ganhados, perdidos, empatados, golesFavor, golesContra.
Para poder ordenar los equipos debe implantar a interface Comparable, y para poder guardarse con el método writeObject de ObjectOutputStream debe implantar Serializable.
Sobrescribe el método equals para que dos Equipos sean iguales si tienen el mismo nombre (¡¡¡¡implanta hashCode()!!!!)
Los equipos deben guardarse en un fichero “clasificacion.dat”.
El programa debe tener un menú con las siguientes opciones: cargar equipos, añadir equipo, guardar equipos, mostrar clasificación, modificar equipo.
Una vez cargados emplee un objeto de tipo TreeSet para que los ordene correctamente.
Ejercicio 11. Gestión de equipos de baloncesto
Haga un programa para la gestión y clasificación de la liga de baloncesto. La clasificación de los equipos se guarda en un archivo llamado clasificacion.dat.
a) Declare una clase Equipo con los atributos mínimos necesarios: nombre, victorias, derrotas, puntosAfavor a favor, puntosEnContra puntos en contra. Puedes añadir los atributos que te interesen, como ciudad, etc. Tienes libertad para hacerlo, pues, además, te puede servir como práctica.
Tenga en cuenta que los atributos puntos, partidos jugados y diferencia de puntos son atributos derivados que se calculan a partir de los partidos ganados, perdidos, puntos a favor y puntos en contra.
Cree los métodos que considere oportunos, pero tome decisiones sobre los métodos get/set necesarios. Así, haz un método que devuelva los puntos, getPuntos, un método getPartidosJugados que devuelva el número de partidos jugados y un método getDiferenciaDePuntos, que devuelva la diferencia de puntos. Obviamente, por ser atributos/propiedades derivados/as, no tienen sentido los métodos de tipo “set” para ellos.
Debe tener, al menos, un constructor para la clase equipo que recoja el nombre y otro que recoja todas las propiedades. No debe existir un constructor por defecto (en la práctica sí si debería tener).
Para poder ordenar los equipos debe implantar la interface Comparable<Equipo>. Piense que debe ordenar por puntos y, a igualdad de puntos, por diferencia de puntos encestados. Además, para poder guardar los objetos (writeObject de ObjectOutputStream) y/o recuperarlos (readObject de ObjectInputStream) debe implantar la interface Serializable. Lo mismo con la clase siguiente, Clasificacion, que debe implementar la interface Serializable.
Sobrescribe el método equals para que se considere que dos Equipos son iguales si tienen el mismo nombre (sin distinguir mayúsculas de minúsculas). Haz lo mismo con hashCode.
b) Declare una clase Clasificacion, con un atributo equipos de tipo ArrayList de Equipo, aunque debe existir un constructor que permita crear una clasificación con los equipos que se desee.
Defina los métodos para añadir equipos a la clasificación, addEquipo, así como los métodos para eliminar equipo, removeEquipo, y sobrescriba el método toString que devuelva la cadena de la clasificación (StringBuilder)
Crea los métodos estáticos: **loadClasificacion**, que cargue la clasificación del archivo y la devuelva, y el método **saveClasificacion**, que guarde la clasificación en el archivo.
Una vez cargados se podría emplear un objeto de tipo TreeSet para que ordene correctamente la clasificación (lo veremos en unidades posteriores)
c) El programa debe tener un menú con las siguientes opciones:
a. añadir equipo (pide el nombre del equipo y los valores de los atributos no derivados, añadiendo el equipo a la clasificación)
b. mostrar clasificación (muestra la clasificación ordenada de los equipos que están cargados en memoria)
c. guardar clasificación (que guarda la clasificación en el archivo clasificacion.dat)
d. cargar clasificación (que carga la clasificación del archivo clasificacion.dat)
e. salir (sale del programa, debiendo preguntar antes).
Utilice la clase Scanner para leer de teclado.
Ejercicio 12. Lectura de un archivo BMP
Modificación de un archivo BMP.
Haga un programa que lea la cabecera de un archivo BMP sin compresión de 24 bits (un archivo de 24 bits implica que cada pixel se representa con 3 bytes, uno para cada color RGB) y muestre la información de la cabecera.
Emplee un flujo de tipo DataInputStream.
La clase DataInputStream permite leer datos primitivos de un flujo de entrada en un formato de datos binarios. Cada método de esta clase lee un dato primitivo de un flujo de entrada en un formato de datos binarios adecuado y devuelve el valor correspondiente.
Para leer los bytes de la cabecera, emplee el método readByte() de la clase DataInputStream o puedes leer todos los bytes con el método readFully(byte[] b).
Puedes emplear el método toBinaryString de la clase Integer para mostrar los bytes en binario.
Defina una clase Cabecera que recoja el nombre del archivo y tenga los atributos necesarios para guardar la información del mismo.
/*
* 2 signature, must be 4D42 hex
* 4 size of BMP file in bytes (unreliable)
* 2 reserved, must be zero
* 2 reserved, must be zero
* 4 offset to start of image data in bytes
* 4 size of BITMAPINFOHEADER structure, must be 40
* 4 image width in pixels
* 4 image height in pixels
* 2 number of planes in the image, must be 1
* 2 number of bits per pixel (1, 4, 8, or 24)
* 4 compression type (0=none, 1=RLE-8, 2=RLE-4)
* 4 size of image data in bytes (including padding)
* 4 horizontal resolution in pixels per meter (unreliable)
* 4 vertical resolution in pixels per meter (unreliable)
* 4 number of colors in image, or zero
* 4 number of important colors, or zero
*/
Ayuda: para pasar el array de 4 bytes a un entero, puede emplear el método ByteBuffer.wrap(byte[]).order(ByteOrder.LITTLE_ENDIAN).getInt(). En el caso de dos bytes puedes emplear ByteBuffer.wrap(byte[]).order(ByteOrder.LITTLE_ENDIAN).getShort().
También puedes hacer uso del siguiente método, que trabaja a más bajo nivel:
La máscara es necesaria porque Java no tiene tipos sin signo y al hacer el cast a int, los bytes se convierten a enteros con signo. Por ejemplo: si el byte es 0xFF (255), al convertirlo a entero, se convierte en -1.
El operador desplazamiento a la izquierda (<<) desplaza los bits a la izquierda y rellena con ceros a la derecha. Si el tipo de dato es byte, se convierte a int antes de hacer la operación.
Diseña e implanta de un programa que lea la cabecera de un BMP y permita invertir la imagen, pasarla a escala de grises, añadir ruido, aclarar y oscurecer. La imagen está a continuación de la cabecera. Para pasar la escala de grises hay que establecer los 3 colores del píxel al mismo nivel con la media de los colores.
Solución parcial lectura archivo BMP
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
publicclassLeerCabeceraBMP {
publicstaticvoidmain(String[] args) {
try (DataInputStream dis =new DataInputStream(new FileInputStream("imagen.bmp"))) {
byte[] cabecera =newbyte[54];
dis.readFully(cabecera); // Guardamos la cabecera en un array de bytes System.out.println("Cabecera BMP:");
System.out.println("Signature: "+new String(cabecera, 0, 2)); // La sinatura BMP es 4D42, de 2 bits System.out.println("Size: "+ byteAInt(cabecera, 2)); // Convierte los 4 bytes a un entero en formato LITTLE_ENDIAN System.out.println("Offset: "+ byteAInt(cabecera, 10)); // Offset a los datos de la imagen System.out.println("Width: "+ byteAInt(cabecera, 18)); // Ancho de la imagen System.out.println("Height: "+ byteAInt(cabecera, 22)); // Alto de la imagen System.out.println("Bits per pixel: "+ byteAInt(cabecera, 28)); // Bits por pixel } catch (IOException e) {
System.out.println("Error de entrada/salida: "+ e.getMessage());
}
}
publicstaticintbyteAInt(byte[] bytes, int offset) {
return ((bytes[offset + 3]& 0xFF) << 24) | ((bytes[offset + 2]& 0xFF) << 16) | ((bytes[offset + 1]& 0xFF) << 8) | (bytes[offset]& 0xFF);
}
}
Solución completa Cabecera archivo BMP
package com.javhoz.ad.e06bmp;
/**
*
* @author pepecalo
*//*
* 2 signature, must be 4D42 hex
* 4 size of BMP file in bytes (unreliable)
* 2 reserved, must be zero
* 2 reserved, must be zero
* 4 offset to start of image data in bytes
* 4 size of BITMAPINFOHEADER structure, must be 40
* 4 image width in pixels
* 4 image height in pixels
* 2 number of planes in the image, must be 1
* 2 number of bits per pixel (1, 4, 8, or 24)
* 4 compression type (0=none, 1=RLE-8, 2=RLE-4)
* 4 size of image data in bytes (including padding)
* 4 horizontal resolution in pixels per meter (unreliable)
* 4 vertical resolution in pixels per meter (unreliable)
* 4 number of colors in image, or zero
* 4 number of important colors, or zero
*/import java.io.*;
publicclassCabeceraBMP {
publicstaticfinal String BARRA ="==========================================";
publicstaticfinal String NL = System.lineSeparator();
publicstaticfinalint TAMANHO = 54;
privatebyte[] cabeceraBytes;
publicCabeceraBMP(String arquivo) {
this(new File(arquivo));
}
publicCabeceraBMP(File f) {
this.cabeceraBytes=newbyte[TAMANHO];
try ( DataInputStream dataInputStream =new DataInputStream(
new FileInputStream(f));) {
dataInputStream.readFully(cabeceraBytes);
} catch (FileNotFoundException ex) {
System.err.println(ex.getMessage());
} catch (IOException ex) {
System.err.println(ex.getMessage());
}
}
public String getSinature() {
returnnew String(cabeceraBytes, 0, 2);
}
publicintgetTamanoArquivo() {
return byteArrayToInt(cabeceraBytes, 2);
}
publicintgetReserva1() {
return byteArrayToShort(cabeceraBytes, 6);
}
publicintgetReserva2() {
return byteArrayToShort(cabeceraBytes, 8);
}
publicintgetOffsetImage() {
return byteArrayToInt(cabeceraBytes, 10);
}
publicintgetInfoHeader() {
return byteArrayToInt(cabeceraBytes, 14);
}
publicintgetAnchura() {
return byteArrayToInt(cabeceraBytes, 18);
}
publicintgetAltura() {
return byteArrayToInt(cabeceraBytes, 22);
}
publicintgetNumeroPlanos() {
return byteArrayToShort(cabeceraBytes, 26);
}
publicintgetBitsPerPixel() {
return byteArrayToShort(cabeceraBytes, 28);
}
publicintgetTipoCompresion() {
return byteArrayToInt(cabeceraBytes, 30);
}
public String getTipoCompresionAsString() {
int tipo = getTipoCompresion();
switch (tipo) {
case 0 -> {
return"Sin compresión";
}
case 1 -> {
return"RLE-8";
}
case 2 -> {
return"RLE-4";
}
default->thrownew AssertionError();
}
}
publicintgetTamanoImagen() {
return byteArrayToInt(cabeceraBytes, 34);
}
publicintgetResolucionHorizontalPorMetro() {
return byteArrayToInt(cabeceraBytes, 38);
}
publicintgetResolucionVerticalPorMetro() {
return byteArrayToInt(cabeceraBytes, 42);
}
publicintgetNumeroColores() {
return byteArrayToInt(cabeceraBytes, 46);
}
publicintgetImportanciaColores() {
return byteArrayToInt(cabeceraBytes, 50);
}
// Método para convertir un array de bytes en un entero de 4 bytes (little endian)publicstaticintbyteArrayToInt(byte[] bytes, int offset) {
return ((bytes[offset + 3]& 0xFF) << 24)
| ((bytes[offset + 2]& 0xFF) << 16)
| ((bytes[offset + 1]& 0xFF) << 8)
| (bytes[offset]& 0xFF);
}
publicstaticintbyteArrayToShort(byte[] bytes, int offset) {
return ((bytes[offset + 1]& 0xFF) << 8)
| (bytes[offset]& 0xFF);
}
/*
* 4 horizontal resolution in pixels per meter (unreliable)
* 4 vertical resolution in pixels per meter (unreliable)
* 4 number of colors in image, or zero
* 4 number of important colors, or zero
*/@Overridepublic String toString() {
StringBuilder sb =new StringBuilder();
sb.append("Cabecera BMP:\n").append(BARRA)
.append("Firma: ").append(getSinature()).append(NL)
.append("Tamaño arquivo: ").append(getTamanoArquivo()).append(NL)
.append("Reserva 1: ").append(getReserva1()).append(NL)
.append("Reserva 2: ").append(getReserva2()).append(NL)
.append("Offset datos imagen: ").append(getOffsetImage()).append(NL)
.append("BITMAPINFOHEADER: ").append(getInfoHeader()).append(NL)
.append("Anchura: ").append(getAnchura()).append(" píxeles").append(NL)
.append("Altura: ").append(getAltura()).append(" píxeles").append(NL)
.append("Número de planos: ").append(getNumeroPlanos()).append(NL)
.append("Bits por píxel: ").append(getBitsPerPixel()).append(NL)
.append("Tipo de compresión: ").append(getTipoCompresionAsString()).append(NL)
.append("Tamaño de la imagen: ").append(getTamanoImagen()).append(" bytes").append(NL)
.append("Resolución horizontal: ").append(getResolucionHorizontalPorMetro()).append(NL)
.append("Resolución vertical: ").append(getResolucionVerticalPorMetro()).append(NL)
.append("Número de colores: ").append(getNumeroColores()).append(NL)
.append("Importancia de colores: ").append(getImportanciaColores()).append(NL);
return sb.toString();
}
publicstaticvoidmain(String[] args) {
// Ruta del archivo BMP a leer String archivoBMP ="e:\\putin.bmp"; // CabeceraBMP cabecera =new CabeceraBMP(archivoBMP);
System.out.println(cabecera);
}
}
Solución pasar a escala de crises archivo BMP
import java.io.*;
publicclassEscalaGrisesBMP {
publicstaticvoidmain(String[] args) {
try (DataInputStream dis =new DataInputStream(new FileInputStream("e:\\putin.bmp"))) {
byte[] cabecera =newbyte[54];
dis.readFully(cabecera); // Guardamos la cabecera en un array de bytesint ancho = ((cabecera[21]& 0xFF) << 24) | ((cabecera[20]& 0xFF) << 16) | ((cabecera[19]& 0xFF) << 8) | (cabecera[18]& 0xFF);
int alto = ((cabecera[25]& 0xFF) << 24) | ((cabecera[24]& 0xFF) << 16) | ((cabecera[23]& 0xFF) << 8) | (cabecera[22]& 0xFF);
int bitsPorPixel = ((cabecera[29]& 0xFF) << 8) | (cabecera[28]& 0xFF);
int tamanoImagen = ((cabecera[37]& 0xFF) << 24) | ((cabecera[36]& 0xFF) << 16) | ((cabecera[35]& 0xFF) << 8) | (cabecera[34]& 0xFF);
byte[] imagen =newbyte[tamanoImagen];
dis.readFully(imagen); // Guardamos la imagen en un array de bytesbyte[] imagenGrises =newbyte[tamanoImagen];
for (int i = 0; i < tamanoImagen; i += 3) {
byte promedio = (byte) ((imagen[i]+ imagen[i + 1]+ imagen[i + 2]) / 3);
imagenGrises[i]= promedio;
imagenGrises[i + 1]= promedio;
imagenGrises[i + 2]= promedio;
}
try (DataOutputStream dos =new DataOutputStream(new FileOutputStream("imagen_grises.bmp"))) {
dos.write(cabecera);
dos.write(imagenGrises);
}
} catch (IOException e) {
System.out.println("Error de entrada/salida: "+ e.getMessage());
}
}
}
Solución añadir ruido archivo BMP
import java.io.*;
publicclassRuidoBMP {
publicstaticvoidmain(String[] args) {
try (DataInputStream dis =new DataInputStream(new FileInputStream("e:\\putin.bmp"))) {
byte[] cabecera =newbyte[54];
dis.readFully(cabecera); // Guardamos la cabecera en un array de bytesint ancho = ((cabecera[21]& 0xFF) << 24) | ((cabecera[20]& 0xFF) << 16) | ((cabecera[19]& 0xFF) << 8) | (cabecera[18]& 0xFF);
int alto = ((cabecera[25]& 0xFF) << 24) | ((cabecera[24]& 0xFF) << 16) | ((cabecera[23]& 0xFF) << 8) | (cabecera[22]& 0xFF);
int bitsPorPixel = ((cabecera[29]& 0xFF) << 8) | (cabecera[28]& 0xFF);
int tamanoImagen = ((cabecera[37]& 0xFF) << 24) | ((cabecera[36]& 0xFF) << 16) | ((cabecera[35]& 0xFF) << 8) | (cabecera[34]& 0xFF);
byte[] imagen =newbyte[tamanoImagen];
dis.readFully(imagen); // Guardamos la imagen en un array de bytesbyte[] imagenRuido =newbyte[tamanoImagen];
for (int i = 0; i < tamanoImagen; i++) {
imagenRuido[i]= (byte) (imagen[i]+ (Math.random() * 255 - 128));
}
try (DataOutputStream dos =new DataOutputStream(new FileOutputStream("imagen_ruido.bmp"))) {
dos.write(cabecera);
dos.write(imagenRuido);
}
} catch (IOException e) {
System.out.println("Error de entrada/salida: "+ e.getMessage());
}
}
}
Tarea 01. Clases DAO con acceso a ficheros.
Tarea: Gestión de equipos y clasificaciones
Haga un programa para la gestión y clasificación de las ligas, como la ACB. Las clasificaciones de los equipos se guardan en archivos binarios o de texto, según decidas. Por ejemplo: Liga ACB.dat.
a) Declare una clase Equipo con los atributos mínimos necesarios: nombre, victorias, derrotas, puntosAfavor a favor, puntosEnContra puntos en contra. Puedes añadir los atributos que te interesen, como ciudad, etc. Tienes libertad para hacerlo, pues, además, te puede servir como práctica. En una liga de fútbol, por ejemplo, se podría añadir el campo estadio y los puntos a favor serían los goles a favor.
Además, ten en cuenta que los atributos puntos, partidos jugados y diferencia de puntos son atributos derivados que se calculan a partir de los partidos ganados, perdidos, puntos a favor y puntos en contra.
Cree los métodos que considere oportunos, pero tome decisiones sobre los métodos get/set necesarios. Así, haz un método que devuelva los puntos, getPuntos, un método getPartidosJugados que devuelva el número de partidos jugados y un método getDiferenciaDePuntos, que devuelva la diferencia de puntos. Obviamente, por ser atributos/propiedades derivados/as, no tienen sentido los métodos de tipo “set” para ellos.
Debe tener, al menos, un constructor para la clase equipo que recoja el nombre y otro que recoja todas las propiedades. No debe existir un constructor por defecto (en la práctica sí si debería tener).
Para poder ordenar los equipos debe implantar la interface Comparable<Equipo>. Piense que debe ordenar por puntos y, a igualdad de puntos, por diferencia de puntos encestados. Además, para poder guardar los objetos (writeObject de ObjectOutputStream) y/o recuperarlos (readObject de ObjectInputStream) debe implantar la interface Serializable. Lo mismo con la clase siguiente, Clasificacion, que debe implementar la interface Serializable.
Sobrescribe el método equals para que se considere que dos Equipos son iguales si tienen el mismo nombre (sin distinguir mayúsculas de minúsculas). Haz lo mismo con hashCode.
b) Declare una clase Clasificacion, con los atributos:
equipos de tipo Set de Equipo, aunque debe existir un constructor que permita crear una clasificación con los equipos que se desee.
competicion de tipo String que recoja el nombre de la competición. Por defecto, la competición debe ser “Liga ACB”.
Defina los métodos para añadir equipos a la clasificación, addEquipo, así como los métodos para eliminar equipo, removeEquipo, y sobrescriba el método toString que devuelva la cadena de la clasificación (StringBuilder)
Los constructores de Clasificación deben crear el conjunto de equipos como tipo TreeSet, para que los ordene automáticamente.
c) Interface DAO<T, K> (Data Access Object) es un patrón de diseño que permite separar la lógica de negocio de la lógica de acceso a los datos. Con los siguientes métodos:
e) Crea una clase EquipoFileDAO que implemente la interfaz DAO<Equipo, String>. Debe implantar los métodos de la interface.
Esta clase debe tener un atributo final, path, de tipo Path con la ruta completa al archivo de datos.
Si se emplea ObjectOutput/InputStream, podría tener un atributo ObjectOutputStream y ObjectInputStream. Si se emplea BufferedWriter/Reader, debe tener un atributo BufferedWriter y BufferedReader.
Sin embargo, podría hacerse en cada uno de los métodos de la clase:
Ejemplo de save con ObjectOutputStream personalizado:
En la que la clase EquipoOutputStream es una clase que hereda de ObjectOutputStream y sobrescribe el método writeStreamHeader para que no escriba la cabecera del stream.
f) Cree una clase ClasificacionFileDAO que implemente la interfaz DAO<Clasificacion, String>.
Debe tener un atributo final con la ruta en la que se guardan los datos de la clasificación: ruta. El nombre del archivo debe ser el nombre de la competición seguido de .dat.
Constructor al que se le pasa la ruta, etc.
Para facilitar el trabajo. los métodos de la clase ClasificacionFileDAO pueden hacer uso de la clase EquipoFileDAO.
Por ejemplo, el método save de ClasificacionFileDAO podría ser:
g) El programa debe tener un menú con las siguientes opciones:
a. Añadir equipo (pide el nombre del equipo y los valores de los atributos no derivados, añadiendo el equipo a la clasificación)
b. Mostrar clasificación (muestra la clasificación ordenada de los equipos que están cargados en memoria)
c. Guardar clasificación (que guarda la clasificación en el archivo clasificacion.dat)
d. Cargar clasificación (que carga la clasificación del archivo clasificacion.dat)
e. Salir (sale del programa, debiendo preguntar antes).
Utilice la clase Scanner para leer de teclado.
Como mejora, intenta hacerlo con una aplicación gráfica.
01.02 Java NIO.2
UD 01.02. Java NIO.2
En este apartado estudiaremos:
Uso de la interface Path para trabajar con rutas archivos y directorios.
Creación de Path
Operaciones comunes de Java NIO.2
Métodos y con Path Java NIO.2
Programación funcional con Java NIO.2
En el apartado anterior presentamos la API java.io y disvimos cutimos cómo utilizarla para interactuar con archivos y flujos. En este apartado, nos centraremos en la API de la versión 2 de java.nio, o NIO.2 de manera resumida, para interactuar con archivos. NIO.2 es un acrónimo que significa la segunda versión de la API de Entrada/Salida No Bloqueante, y a veces se conoce como la “New I/O.”
Mostraremos cómo NIO.2 nos permite hacer mucho más con archivos y directorios que la API original de java.io. También te mostraremos cómo aplicar la API de Streams (ojo, no confundir con “streams” de entrada/salida) para realizar operaciones complejas con archivos y directorios. Concluiremos mostrando las diversas formas en que se pueden leer y escribir atributos de archivos utilizando NIO.2.
Presentando NIO.2
En su núcleo, NIO.2 es una sustitución para la antigua clase java.io.File que estudiamos en el apartado anterior. El objetivo de la API es proporcionar una API más intuitiva y rica en funciones para trabajar con archivos y directorios.
Cuando decimos antigua, nos referimos a que el enfoque preferido para trabajar con archivos y directorios en aplicaciones de software más recientes es utilizar NIO.2 en lugar de java.io.File. Como veremos, NIO.2 proporciona muchas características y mejoras de rendimiento que la clase heredada admitía.
La piedra angular de NIO.2 es la interfaz java.nio.file.Path. Una instancia de Path representa una ruta jerárquica en el sistema de almacenamiento hacia un archivo o directorio. Se puede pensar en un Path como el sustitución de NIO.2 para la clase java.io.File, aunque la forma en que se utiliza es un poco diferente.
Antes de profundizar en eso, hablemos de las similitudes entre estas dos implementaciones. Tanto los objetos java.io.File como Path pueden hacer referencia a una ruta absoluta o relativa dentro del sistema de archivos. Además, ambos pueden hacer referencia a un archivo o un directorio. Como hicimos en el apartado de java.io y continuamos haciendo en éste, tratamos a una instancia que apunta a un directorio como un archivo, ya que se almacena en el sistema de archivos con propiedades similares. Por ejemplo, podemos cambiar el nombre de un archivo o directorio con los mismos métodos en ambas APIs.
Ahora, algo completamente diferente. A diferencia de la clase java.io.File, la interfaz Path da soporte para enlaces simbólicos. Un enlace simbólico es un archivo especial dentro de un sistema de archivos que sirve como una referencia o puntero a otro archivo o directorio. La figura siguiente muestra un enlace simbólico desde /zoo/favorite a /zoo/cats/lion:
En imagen anterior, la carpeta lion y sus elementos se pueden acceder directamente o a través del enlace simbólico. Por ejemplo, las siguientes rutas hacen referencia al mismo archivo:
/zoo/cats/lion/Cubs.java
/zoo/favorite/Cubs.java
En general, los enlaces simbólicos son transparentes para el usuario, ya que el sistema operativo se encarga de resolver la referencia al archivo real. Java NIO.2 incluye soporte completo para crear, detectar y navegar enlaces simbólicos dentro del sistema de archivos.
Dado que Path es una interfaz, no podemos crear una instancia directamente. ¡Después de todo, las interfaces no tienen constructores! Java proporciona varias clases y métodos que puedes usar para obtener objetos de tipo Path (dos, o casi).
¿Por qué Path es una interface? Cuando se crea un Path, la máquina virtual de de Java devuelve la implementación específica para el sistema de archivos subyacente. Por ejemplo, la ruta no es igual para Linux que para Windows.
En la mayoría de las circunstancias se desea realizar las mismas operaciones con el Path, independientemente del sistema de archivos.
La API de Java proporciona Path como0 una interface usando el patrón de diseño Factory (lo veremos más adelante en el curso), que nos evita escribir código complejo o personalizado para cada sistenma de archivos.
1.1. Creando un Path con Path.of
La forma más simple y directa de obtener un objeto Path es utilizar el método Factory estático definido dentro de la interfaz Path.
// Método Factory de Pathpublicstatic Path of(String first, String... more)
Es fácil crear instancias de Path a partir de valores de String:
El primer ejemplo crea una referencia a una ruta relativa en el directorio de trabajo actual. El segundo ejemplo crea una referencia a una ruta de archivo absoluta en un sistema basado en Windows. El tercer ejemplo crea una referencia a una ruta de directorio absoluta en un sistema basado en Linux o Mac.
Rutas absolutas vs. relativas
Determinar si una ruta es relativa o absoluta depende del sistema de archivos. Convenciones:
Si una ruta comienza con una barra inclinada hacia adelante (/), es absoluta, con / como el directorio raíz. Ejemplos: /home/foto.png y /no/../hay/./cole
Si una ruta comienza con una letra de unidad (c:), es absoluta, con la letra de unidad como el directorio raíz. Ejemplos: c:/una/vacaloca.png y d:/tren/../rojo/./verde
De lo contrario, es una ruta relativa. Ejemplos: fotos/violin.png y tren/../rojo/./verde
Recuerda . representa el directorio actual y .. el directorio padre.
El método Path.of() también incluye varargs (argumentos variables) para pasar elementos de ruta adicionales. Los valores se combinarán y se separarán automáticamente por el separador de archivos dependiente del sistema operativo que aprendiste en el aparado anterior.
Estos ejemplos son simplemente otro modo de escribir los ejemplos anterioresvde Path, utilizando la lista de parámetros de valores String en lugar de un solo valor String. La ventaja de varargs es que es más robusto, ya que inserta el separador de ruta del sistema operativo adecuado por ti (sin tener que poner / o \).
1.2. Creando un Path con Paths.get
El método Path.of() se introdujo en Java 11. Otra forma de obtener una instancia de Path es desde la clase Factory java.nio.file.Paths (empleada para crear objetos). Ten en cuenta la ’s’ al final de la clase Paths para distinguirla de la interfaz Path.
Paths.get() es más “antiguo”, pero puede usarse tanto Path.of() como Paths.get() de manera totalmente intercambiable.
1.3. Creando un Path de URI: Path.of, Paths.get
Otra forma de construir un Path usando la clase Paths es con un valor de URI. Un identificador uniforme de recursos (URI) es una cadena de caracteres que identifica un recurso (remoto o local). Comienza con un esquema que indica el tipo de recurso, seguido de un valor de ruta. Ejemplos de valores de esquema incluyen file:// para sistemas de archivos locales, y http://, https:// y ftp:// para sistemas de archivos remotos.
La clase java.net.URI se utiliza para crear valores de URI.
// Constructor de URIpublicURI(String str) throws URISyntaxException
Java incluye varios métodos Factory para la conversión entre objetos Path y URI, creación de Path y creación de URI.
// De URI a Path, usando el método Factory de Pathpublicstatic Path of(URI uri)
// De URI a Path, usando el método Factory de Pathspublicstatic Paths get(URI uri)
// De Path a URI, usando el método de instancia de Path:public URI toURI()
Los siguientes ejemplos hacen referencia al mismo archivo (ojo no está implantado para http y https, en principio):
URI a =new URI("file://nohaycole.txt");
Path b = Path.of(a); // Creación de una Path a partir de una URL.Path c = Paths.get(a); // Creacación de un Path a partir de una URL.URI d = b.toUri(); // Conversión de un Path en una URL.
Algunos de estos ejemplos pueden lanzar una IllegalArgumentException en tiempo de ejecución, ya que algunos sistemas requieren que los URI sean absolutos. La clase URI tiene un método isAbsolute(), aunque se refiere a si el URI tiene un esquema, no a la ubicación del archivo.
1.4. Obteniendo un Path con FileSystem.getPath
Java NIO.2 hace un uso extensivo de la creación de objetos con clases con el patrón Factory. Como ya hermos visto, la clase Paths crea instancias de la interfaz Path.
Del mismo modo, la clase FileSystems crea instancias de la clase abstracta FileSystem.
// Método Factory de FileSystemspublicstatic FileSystem getDefault()
La clase FileSystem incluye métodos para trabajar directamente con el sistema de archivos. De hecho, tanto Paths.get() como Path.of() son en realidad atajos para este método de FileSystem:
// Método de instancia de FileSystempublic Path getPath(String first, String... more)
Reescribamos una vez más nuestros tres ejemplos anteriores para mostrar cómo obtener una instancia de Path “a la antigua”:
Si bien la mayor parte del tiempo queremos acceso a un objeto Path que esté dentro del sistema de archivos local, la clase FileSystems nos da la *libertad para conectarnos a un sistema de archivos remoto+, de la siguiente manera:
// Método Factory de FileSystemspublicstatic FileSystem getFileSystem(URI uri)
Lo siguiente muestra cómo se puede usar este método:
Este código es útil cuando necesitamos construir objetos Path con frecuencia para un sistema de archivos remoto. NIO.2 nos permite conectarnos tanto a sistemas de archivos locales como remotos, lo cual es una mejora importante sobre la antigua clase java.io.File.
1.5. Creando un Path a partir de un java.io.File: toPath()
Por último, pero no menos importante, podemos obtener instancias de Path utilizando la antigua clase java.io.File. De hecho, también podemos obtener un objeto java.io.File a partir de una instancia de Path.
// De Path a File, usando el método de instancia de Path:publicdefault File toFile()
// De File a Path, usando el método de instancia de java.io.File:public Path toPath()
Estos métodos están disponibles por conveniencia y también para ayudar a facilitar la integración entre las API antiguas y las nuevas. Ejemplos:
Sin embargo, al trabajar con aplicaciones más actuales, se recomienda el uso de Path de NIO.2, ya que contiene muchas más características.
Resumen de las relaciones entre clases de NIO.2
A estas alturas, deberías darte cuenta de que NIO.2 hace un uso extensivo del patrón Factory, cuyo uso es sencillo pero estudiaremos más adelante. Muchas de tus interacciones con Java NIO.2 requieren dos tipos: una clase o interfaz abstracta y una clase Factory o auxiliar. Siguiente imagen muestra las relaciones entre las clases de NIO.2, así como algunas clases principales de java.io y java.net. Relaciones de clases e interfaces de NIO.2:
Revisa la imagen cuidadosamente. Al trabajar con NIO.2, fíjate si el nombre de la clase es singular o plural. Las clases con nombres en plural incluyen métodos para crear u operar en instancias de clases/interfaces con nombres en singular. Recuerda, un Path también se puede crear a partir de la interfaz Path, utilizando el método estático of().
Incluida en el esquema está la clase java.nio.file.Files, que veremos más adelante en detalle. Se trata de una clase auxiliar o de utilidad que opera principalmente en instancias de Path para leer o modificar archivos y directorios reales.
A lo largo de este capítulo, veremos numerosos métodos que deberías conocer de Java NIO.2. Antes de entrar en los detalles de cada método, mostraremos algunas funciones comunes a modo introductorio.
Símbolos para rutas
Las rutas absolutas y relativas pueden contener símbolos de ruta. Un símbolo de ruta es una serie reservada de caracteres que tienen un significado especial dentro de algunos sistemas de archivos. Hay dos símbolos básicos (elementales) de ruta que debes conocer, como se indica en la siguiente tabla:
Símbolos de sistema de archivos
Símbolo
Descripción
.
Referencia al directorio actual
..
Referencia al directorio padre del directorio actual
Ilustramos el uso de los símbolos de ruta en la siguiente figura:
En la figura anterior, el directorio actual es /fish/shark/hammerhead. En este caso, ../swim.txt se refiere al archivo swim.txt en el directorio padre del directorio actual. De manera similar, ./play.png se refiere a play.png en el directorio actual. Estos símbolos también se pueden combinar para un mayor efecto. Por ejemplo, ../../clownfish se refiere al directorio que está dos directorios arriba del directorio actual.
A veces verás símbolos de ruta que son redundantes o innecesarios. Por ejemplo, la ruta absoluta /fish/shark/hammerhead/.././swim.txt se puede simplificar a /fish/shark/swim.txt. Veremos cómo manejar estas redundancias más adelante en el capítulo cuando cubriremos normalize().
Argumentos Opcionales en métodos de NIO.2
Muchos de los métodos de java NIO.2 incluyen un varargs que toma una lista opcional de valores. En la siguiente tabla se presentan los argumentos con los que deberías estar familiarizado, por lo menos su existencia. Argumentos comunes de los métodos de NIO.2
Tipo de Enum
Interfaz Heredada
Valor de Enum
Detalles
LinkOption
CopyOption, OpenOption
NOFOLLOW_LINKS
No seguir enlaces simbólicos.
StandardCopyOption
CopyOption
ATOMIC_MOVE
Mover archivo como operación atómica del sistema de archivos.
COPY_ATTRIBUTES
Copiar atributos existentes al nuevo archivo.
REPLACE_EXISTING
Sobrescribir el archivo si ya existe.
StandardOpenOption
OpenOption
APPEND
Si el archivo ya está abierto para escribir, entonces añadir al final.
CREATE
Crear un nuevo archivo si no existe.
CREATE_NEW
Crear un nuevo archivo solo si no existe, fallar en caso contrario.
READ
Abrir para acceso de lectura.
TRUNCATE_EXISTING
Si el archivo ya está abierto para escribir, entonces borrar el archivo y añadir al principio.
WRITE
Abrir para acceso de escritura.
Con las excepciones de Files.copy() y Files.move() (que cubriremos más adelante), no profundizaremos en estos parámetros varargs cada vez que presentemos un método. Aunque el comportamiento de ellos debería ser directo. Por ejemplo, ¿puedes entender lo que hace la siguiente llamada a Files.exists() con LinkOption en el siguiente fragmento de código?
El Files.exists() simplemente verifica si un archivo existe. Sin embargo, si el parámetro es un enlace simbólico, entonces el método verifica si el objetivo del enlace simbólico existe en su lugar. Proporcionar LinkOption.NOFOLLOW_LINKS significa que el comportamiento predeterminado será anulado, y el método verificará si el enlace simbólico en sí existe.
Ten en cuenta que algunos de los enums en tabla anterior heredan una interfaz. Eso significa que algunos métodos aceptan una variedad de tipos de enums. Por ejemplo, el método Files.move() toma un CopyOption vararg para que pueda aceptar enums de diferentes tipos.
Muchos de los métodos presentados en este apartado lanzan (pueden lanzar) una IOException. Las causas comunes de que un método lance esta excepción incluyen:
Pérdida de comunicación con el sistema de archivos subyacente.
El archivo o directorio existe pero no se puede acceder o modificar. El archivo existe pero no se puede sobrescribir.
Se requiere el archivo o directorio pero no existe.
En general, los métodos que operan en valores abstractos de Path, como los de la interfaz Path o la clase Paths, a menudo no lanzan ninguna excepción verificada. Por otro lado, los métodos que operan o cambian archivos y directorios, como los de la clase Files, a menudo declaran IOException.
Hay excepciones a esta regla, como veremos. Por ejemplo, el método Files.exists() no declara IOException. Si lanzara una excepción cuando el archivo no existiera, ¡nunca podría devolver false!
Hemos visto los conceptos básicos de NIO.2. Ahora veremos cómo Java NIO.2 proporciona una gran cantidad de métodos y clases que operan en objetos Path, muchos más de los que estaban disponibles en la API java.io. En esta sección, presentamos los métodos de Path más importantes.
Al igual que los valores de String, entre otros objetos, las instancias de Path son inmutables. En el siguiente ejemplo, la operación Path en la segunda línea se pierde ya que p es inmutable:
Path p = Path.of("ballena");
p.resolve("krill"); // Se pierda, debería guardarse en otro Path.System.out.println(p); // ballena
Muchos de los métodos disponibles en la interfaz Path transforman de alguna manera el valor del path y devuelven un nuevo objeto Path, permitiendo encadenar los métodos. Demostramos el encadenamiento en el siguiente ejemplo, cuyos detalles discutiremos en esta sección del capítulo:
Muchos de los fragmentos de código ede esta unidad que hemos visto se pueden ejecutar sin que las rutas a las que hacen referencia realmente existan. La JVM se comunica con el sistema de archivos para determinar los componentes de la ruta o el directorio principal de un archivo, sin requerir que el archivo realmente exista. Como regla general, si el método declara una IOException, entonces normalmente requiere que las rutas en las que opera existan.
Métodos principales
1. Visualizando el Path con toString(), getNameCount() y getName()
La interfaz Path contiene tres métodos para recuperar información básica sobre la representación del path.
public String toString() // Devuelve una cadena con el Path completo.ünico que devuelve cadena.publicintgetNameCount()
public Path getName(int index)
El primer método, toString(), devuelve una representación de cadena del path completo. De hecho, es el único método en la interfaz Path que devuelve una cadena. Muchos de los otros métodos en la interfaz Path devuelven instancias de Path.
Los métodos getNameCount() y getName() se usan a menudo en conjunto para recuperar el número de elementos en la ruta y una referencia a cada elemento, respectivamente. Estos dos métodos no incluyen el directorio raíz como parte del path.
Path path = Paths.get("/tierra/hipopotamo/harry.feliz");
System.out.println("El nombre del Path es: "+ path);
for(int i=0; i<path.getNameCount(); i++) {
System.out.println(" Elemento "+ i +" es: "+ path.getName(i));
}
Aunque esta es una ruta absoluta, el elemento raíz no se incluye en la lista de nombres. Como dijimos, estos métodos no consideran el directorio raíz como parte del path.
var p = Path.of("/");
System.out.print(p.getNameCount()); // 0System.out.print(p.getName(0)); // IllegalArgumentException
Observa que si intentas llamar a getName() con un índice no válido, lanzará una excepción en tiempo de ejecución.
2. Creando un nuevo Path con subpath()
La interfaz Path incluye un método para seleccionar partes de un path.
public Path subpath(int beginIndex, int endIndex)
Las referencias son inclusivas del beginIndex y exclusivas del endIndex. El método subpath() es similar al método getName() anterior, excepto que subpath() puede devolver múltiples componentes de la ruta, mientras que getName() devuelve solo uno. Ambos devuelven instancias de Path, sin embargo.
El siguiente fragmento de código muestra cómo funciona subpath(). También imprimimos los elementos del Path usando getName() para que puedas ver cómo se usan los índices.
var p = Paths.get("/mamifero/omnivoro/mapache.imagen");
System.out.println("El Path es: "+ p);
for (int i = 0; i < p.getNameCount(); i++) {
System.out.println(" Elemento "+ i +" es: "+ p.getName(i));
}
System.out.println();
System.out.println("subpath(0,3): "+ p.subpath(0, 3));
System.out.println("subpath(1,2): "+ p.subpath(1, 2));
System.out.println("subpath(1,3): "+ p.subpath(1, 3));
La salida de este fragmento de código es la siguiente:
Al igual que getNameCount() y getName(), subpath() se indexa desde 0 y no incluye el root. También como getName(), subpath() arroja una excepción si se proporcionan índices no válidos.
var q = p.subpath(0, 4); // IllegalArgumentExceptionvar x = p.subpath(1, 1); // IllegalArgumentException
El primer ejemplo arroja una excepción en tiempo de ejecución, ya que el valor máximo de índice permitido es 3. El segundo ejemplo arroja una excepción ya que los índices de inicio y fin son iguales, lo que lleva a un valor de ruta vacío.
3. Accediendo a los elementos del Path con getFileName(), getParent() y getRoot()
La interfaz Path contiene numerosos métodos para recuperar elementos específicos de un Path, devueltos como objetos Path por sí mismos.
public Path getFileName()
public Path getParent()
public Path getRoot()
El método getFileName() devuelve el elemento Path del archivo o directorio actual, mientras que getParent() devuelve la ruta completa del directorio contenedor. getParent() devuelve null si se opera en la ruta raíz o en la parte superior de una ruta relativa. El método getRoot() devuelve el elemento raíz del archivo dentro del sistema de archivos, o null si la ruta es relativa.
Considera el siguiente método, que imprime varios elementos de Path:
publicvoidprintPathInformation(Path path) {
System.out.println("Nombre del archivo: "+ path.getFileName());
System.out.println("Raíz es: "+ path.getRoot());
Path currentParent = path;
while ((currentParent = currentParent.getParent()) !=null) {
System.out.println(" Directorio actual es: "+ currentParent);
}
}
El bucle while en el método printPathInformation() continúa hasta que getParent() devuelve null. Aplicamos este método a las siguientes tres rutas:
Esta aplicación de prueba produce la siguiente salida:
Nombre del archivo: zoo Raíz es: null
Nombre del archivo: shells.txt Raíz es: /
Directorio actual es: /zoo/armadillo
Directorio actual es: /zoo
Directorio actual es: .
Revisando la salida de prueba, puedes ver la diferencia en el comportamiento de getRoot() en rutas absolutas y relativas. Como puedes ver en los primeros y últimos ejemplos, getParent() no atraviesa las rutas relativas fuera del directorio de trabajo actual.
También puedes ver que estos métodos no resuelven los símbolos de ruta y los tratan como una parte distintiva de la ruta. Aunque la mayoría de los métodos en esta parte del capítulo tratarán los símbolos de ruta como parte de la ruta, presentaremos uno próximamente que limpia los símbolos de ruta.
4. Verificando el Tipo de Path con isAbsolute() y toAbsolutePath()
La interfaz Path contiene dos métodos para ayudar con rutas relativas y absolutas:
publicbooleanisAbsolute()
public Path toAbsolutePath()
El primer método, isAbsolute(), devuelve true si la ruta a la que hace referencia el objeto es absoluta y false si la ruta es relativa. Como hemos estudiado anteriormente en este capítulo, si una ruta es absoluta o relativa a menudo depende del sistema de archivos, aunque adoptamos convenciones comunes para simplificar los ejemplos de código.
El segundo método, toAbsolutePath(), convierte un objeto Path relativo en un objeto Path absoluto uniéndolo al directorio de trabajo actual. Si el objeto Path ya es absoluto, el método simplemente devuelve el objeto Path.
El siguiente fragmento de código muestra el uso de ambos métodos al ejecutarse en un sistema Windows y Linux, respectivamente:
var path1 = Paths.get("C:\\birds\\egret.txt");
System.out.println("¿Path1 es Absoluto? "+ path1.isAbsolute());
System.out.println("Path Absoluto1: "+ path1.toAbsolutePath());
var path2 = Paths.get("birds/condor.txt");
System.out.println("¿Path2 es Absoluto? "+ path2.isAbsolute());
System.out.println("Path Absoluto2 "+ path2.toAbsolutePath());
La salida para el fragmento de código en cada sistema respectivo se muestra en la siguiente salida de muestra. Para el segundo ejemplo, supón que el directorio de trabajo actual es /home/work.
¿Path1 es Absoluto? true
Path Absoluto1: C:\birds\egret.txt
¿Path2 es Absoluto? false
Path Absoluto2 /home/work/birds/condor.txt
5. Uniéndo Paths con resolve()
Supongamos que quieres concatenar rutas de manera similar a como concatenamos cadenas. La interfaz Path proporciona dos métodos resolve() para hacer precisamente eso.
public Path resolve(Path other)
public Path resolve(String other)
El primer método toma un parámetro Path, mientras que la versión sobrecargada es una forma abreviada del primero que toma un String (y construye el Path por ti). El objeto sobre el cual se invoca el método resolve() se convierte en la base del nuevo objeto Path, con el argumento de entrada agregado al Path. Veamos qué sucede si aplicamos resolve() a una ruta absoluta y una ruta relativa:
El fragmento de código genera la siguiente salida:
/gatos/../pantera/comida
Al igual que los otros métodos que hemos visto hasta ahora, resolve() no elimina los símbolos de ruta. En este ejemplo, el argumento de entrada al método resolve() era una ruta relativa, pero ¿qué pasa si hubiera sido una ruta absoluta?
Dado que el parámetro de entrada path3 es una ruta absoluta, la salida sería la siguiente:
/tigre/jaula
Para el examen, debes tener en cuenta la mezcla de rutas absolutas y relativas con el método resolve(). Si se proporciona una ruta absoluta como entrada al método, entonces ese es el valor que se devuelve. En pocas palabras, no puedes combinar dos rutas absolutas usando resolve().
6. Derivando un Path con relativize()
La interfaz Path incluye un método para construir la ruta relativa de un Path a otro, a menudo usando símbolos de ruta.
public Path relativize(Path other)
¿Qué crees que imprimirán los siguientes ejemplos usando relativize()?
var path1 = Path.of("pez.txt");
var path2 = Path.of("pajaros/amigables.txt");
System.out.println(path1.relativize(path2));
System.out.println(path2.relativize(path1));
Los ejemplos imprimen lo siguiente:
../pajaros/amigables.txt
../../pez.txt
La idea es la siguiente: si te encuentras en una ruta en el sistema de archivos, ¿qué pasos necesitarías seguir para llegar a la otra ruta? Por ejemplo, para llegar a fish.txt desde friendly/birds.txt, necesitas subir dos niveles (el archivo mismo cuenta como un nivel) y luego seleccionar fish.txt.
Si ambos valores de la ruta son relativos, entonces el método relativize() calcula las rutas como si estuvieran en el mismo directorio de trabajo actual. Alternativamente, si ambos valores de la ruta son absolutos, entonces el método calcula la ruta relativa desde una ubicación absoluta hasta otra, independientemente del directorio de trabajo actual. El siguiente ejemplo demuestra esta propiedad al ejecutarse en una computadora con Windows:
El fragmento de código funciona incluso si no tienes una unidad E: en tu sistema. Recuerda, la mayoría de los métodos definidos en la interfaz Path no requieren que la ruta exista.
El método relativize() requiere que ambas rutas sean absolutas o ambas relativas y arroja una excepción si los tipos están mezclados.
En sistemas basados en Windows, también requiere que si se utilizan rutas absolutas, entonces ambas rutas deben tener el mismo directorio raíz o letra de unidad. Por ejemplo, lo siguiente también arrojaría una IllegalArgumentException en un sistema basado en Windows:
Hasta ahora, hemos presentado varios ejemplos que incluyen símbolos de ruta innecesarios. Afortunadamente, Java proporciona un método para eliminar redundancias innecesarias en una ruta.
public Path normalize()
Recuerda, el símbolo de ruta .. se refiere al directorio padre, mientras que el símbolo de ruta . se refiere al directorio actual. Podemos aplicar normalize() a algunas de nuestras rutas anteriores.
Los dos primeros ejemplos aplican los símbolos de ruta para eliminar las redundancias, pero ¿y el último? Esa es tan simplificada como puede ser. El método normalize() no elimina todos los símbolos de ruta; solo aquellos que se pueden reducir.
El método normalize() también nos permite comparar rutas equivalentes. Considera el siguiente ejemplo:
var p1 = Paths.get("/pony/../weather.txt");
var p2 = Paths.get("/weather.txt");
System.out.println(p1.equals(p2)); // falseSystem.out.println(p1.normalize().equals(p2.normalize())); //true
El método equals() devuelve true si dos rutas representan el mismo valor. En la primera comparación, los valores de las rutas son diferentes. En la segunda comparación, los valores de las rutas se han reducido a la misma ruta normalizada, /weather.txt. Esta es la función principal del método normalize(), permitirnos comparar mejor diferentes rutas.
8. Recuperando la Ruta del Sistema de Archivos con toRealPath()
Si bien trabajar con rutas teóricas es útil, a veces quieres verificar que la ruta realmente existe dentro del sistema de archivos.
public Path toRealPath(LinkOption... options) throws IOException
Este método es similar a normalize(), en el sentido de que elimina cualquier símbolo de ruta redundante. También es similar a toAbsolutePath(), en el sentido de que unirá la ruta con el directorio de trabajo actual si la ruta es relativa.
Sin embargo, a diferencia de esos dos métodos, toRealPath() arrojará una excepción si la ruta no existe. Además, seguirá enlaces simbólicos, con un parámetro varargs opcional para ignorarlos.
Supongamos que tenemos un sistema de archivos en el que tenemos un enlace simbólico desde /cebra a /caballo. ¿Qué crees que imprimirá lo siguiente, dado un directorio de trabajo actual de /caballo/horario?
En este ejemplo, tanto las rutas absolutas como las relativas resuelven al mismo archivo absoluto, ya que el enlace simbólico apunta a un archivo real dentro del sistema de archivos.
También podemos usar el método toRealPath() para acceder al directorio de trabajo actual como un objeto Path.
System.out.println(Paths.get(".").toRealPath());
Resumen de los métodos de Path
A modo de resumen, muostramos los métodos de Path que deberías, al menos, haber probado:
Métodos de Path
Métodos de Path
Path of(String, String…)
Path getParent()
URI toURI()
Path getRoot()
File toFile()
boolean isAbsolute()
String toString()
Path toAbsolutePath()
int getNameCount()
Path relativize()
Path getName(int)
Path resolve(Path)
Path subpath(int, int)
Path normalize()
Path getFileName()
Path toRealPath(LinkOption…)
Salvo el método estático Path.of(), todos los métodos en mostrados son métodos de instancia que se pueden llamar en cualquier instancia de Path. Además, solo toRealPath() declara una IOException.
La programación funcional de Java NIO.2 realizar operaciones de archivo extremadamente poderosas, a menudo con sólo unas pocas líneas de código.
La clase Files incluye algunos métodos muy útiles de la API Stream que operan en archivos, directorios y árboles de directorio: find,lines, list, walk.
El método Files.list() es similar al método listFiles() de java.io.File, excepto que devuelve un Stream<Path> en lugar de un array de File File[]. Además, listFiles es un método de instancia, no estático:
public File[]listFiles()
Dado que los streams utilizan la evaluación “perezosa”, esto significa que el método cargará cada elemento del directorio según sea necesario, en lugar de cargar todo el directorio de una vez.
Por ejemplo, puede imprimir el contenido de un directorio con el siguiente código (se obvia la excepción que debe capturarse):
try (Stream<Path> s = Files.list(Path.of("/home"))) {
s.forEach(System.out::println);
}
Recuerda que el método forEach de Stream se declara del siguiente modo :
voidforEach(Consumer<?super T> action)
En este caso, sería un Consumer<? super Path> por lo que debe implantar un método accept que no devuelve nada y recoge un Path (o super de Path):
Hagamos algo más interesante. Recordad que existe el método Files.copy() y que solo realiza una copia superficial de un directorio. Podemos usar Files.list() para realizar una copia profunda de un directorio en otro.
El primer método copia la ruta, ya sea un archivo o un directorio. Si es un directorio, se realiza solo una copia superficial. Luego, verifica si la ruta es un directorio y, si lo es, realiza una copia recursiva de cada uno de sus elementos. ¿Y si el método se encuentra con un enlace simbólico? De momento, la JVM no seguirá enlaces simbólicos al usar este método de stream, pero hay forma de hacerlo.
Ejercicio
Realiza un programa que copie todos los archivos *.java (incluidos subdirectorios) en un directorio destinio.
Si el directorio destino no existe debe crearlo.
Recorre el directorio y si es un directorio invócalo recursivamente.
Filtra de modo que el nombre del archivo termine en .java
3. Cierre del Stream
En los dos últimos ejemplos de código, colocamos los objetos Stream dentro de un bloque try-with-resources.
Deben cerrarse los streams a archivos.
Los métodos basados en streams de NIO.2 abren una conexión al sistema de archivos que debe cerrarse correctamente, o de lo contrario podría producirse una fuga de recursos.
Una fuga de recursos dentro del sistema de archivos significa que la ruta puede estar bloqueada para su modificación mucho después de que se haya completado el proceso que la utilizó.
Si asumieras que una operación terminal de un stream cerraría automáticamente los recursos de archivo subyacentes, estarías equivocado. (Hubo mucho debate sobre este comportamiento cuando se presentó por primera vez, pero en resumen, se decidió que los desarrolladores deben cerrar el stream).
En el lado positivo, no todos los streams necesitan cerrarse, sólo aquellos que abren recursos, como los que se encuentran en NIO.2. Por ejemplo, no necesitabas cerrar ninguno de los streams de programación funcional.
Aun así, por comodidad, a veces se omite el cierre de recursos de NIO.2 en los ejemplos que mostramos, pero cuando programas, siempre utiliza declaraciones try-with-resources con estos métodos de NIO.2.
4. Recorrido de un árbol de directorios
El Files.list() es útil, recorre sólo el contenido de un solo directorio.
¿Qué pasa si queremos visitar todas las rutas dentro de un árbol de directorios?
REcordad que el sistema de archivos está organizado de manera jerárquica. Por ejemplo, un directorio puede contener archivos y otros directorios, que a su vez pueden contener otros archivos y directorios. Cada registro en un sistema de archivos tiene exactamente un padre, con la excepción del directorio raíz, que se encuentra en la parte superior de todo.
Un sistema de archivos se visualiza comúnmente como un árbol con un solo nodo raíz, con muchas ramas y hojas, como se muestra en la imagen siguiente. En este modelo, un directorio es una rama o nodo interno, y un archivo es un nodo hoja.
Estructura de árbol de archivo y directorio:
Una tarea común en un sistema de archivos es iterar sobre los descendientes de una ruta, ya sea registrando información sobre ellos o, más comúnmente, filtrándolos para un conjunto específico de archivos. Por ejemplo, es posible que desees buscar en una carpeta e imprimir una lista de todos los archivos .java. Además, los sistemas de archivos almacenan los registros de archivos de manera jerárquica. En general, si deseas buscar un archivo, debes comenzar con un directorio principal, leer sus elementos secundarios, luego leer sus hijos, y así sucesivamente.
Recorrer un directorio, también conocido como caminar (walk) por un árbol de directorios, es el proceso por el cual comienzas con un directorio principal e iteras sobre todos sus descendientes hasta que se cumple alguna condición o no hay más elementos sobre los cuales iterar. Por ejemplo, si estamos buscando un solo archivo, podemos finalizar la búsqueda cuando se encuentra el archivo o cuando hemos revisado todos los archivos y no encontramos nada.
La ruta de inicio suele ser un directorio específico; después de todo, sería consumidor de tiempo buscar en todo el sistema de archivos en cada solicitud.
4.01 Búsqueda en profundidad y búsqueda en anchura
Existen dos estrategias comunes asociadas con recorrer un árbol de directorios: una búsqueda en profundidad y una búsqueda en amplitud (estas estragias también se pueden extrapolar a cualquier tipo de árbol).
Una búsqueda en profundidad recorre la estructura desde la raíz hasta una hoja arbitraria y luego navega hacia atrás hacia la raíz, recorriendo completamente los caminos que omitió en el camino.
Profundidad de búsqueda
La profundidad de búsqueda es la distancia desde la raíz hasta el nodo actual. Para evitar búsquedas interminables, Java incluye una profundidad de búsqueda que se utiliza para limitar cuántos niveles (o saltos) desde la raíz se permite que vaya la búsqueda.
Búsqueda en anchura
Alternativamente, una búsqueda en amplitud comienza en la raíz y procesa todos los elementos de cada profundidad particular antes de pasar al siguiente nivel de profundidad.
Los resultados están ordenados por profundidad, con todos los nodos en la profundidad 1 leídos antes de todos los nodos en la profundidad 2, y así sucesivamente. Aunque una búsqueda en anchura tiende a ser equilibrada y predecible, también requiere más memoria ya que debe mantener una lista de nodos visitados.
Los métodos de la API de Streams de NIO.2 utilizan una búsqueda en profundidad con un límite de profundidad, que puede cambiarse opcionalmente.
4.02 “Caminar” por un directorio con walk()
La clase Files incluye dos métodos para recorrer el árbol de directorios utilizando una búsqueda en profundidad.
Al igual que nuestros otros métodos de stream, walk() utiliza la evaluación perezosa (lazy) y evalúa un Path solo cuando llega a él. Esto significa que incluso si el árbol de directorios incluye cientos o miles de archivos, > la memoria requerida para procesar un árbol de directorios es baja.
El primer método walk() se basa en una profundidad máxima predeterminada de Integer.MAX_VALUE, mientras que la versión sobrecargada permite al usuario establecer una profundidad máxima. Esto es útil en casos donde el sistema de archivos puede ser grande y sabemos que la información que estamos buscando está cerca de la raíz.
En lugar de simplemente imprimir el contenido de un árbol de directorios, podemos hacer algo más interesante. El siguiente método getPathSize()recorre un árbol de directorios y devuelve el tamaño total de todos los archivos en el directorio:
Nota: el método LongStream mapToLong(ToLongFunction<? super T> mapper) recoge una interfaz ToLongFunction que debe implantar el método long applyAsLong(T value) que devuelve un long, en nuestro caso empleamos la función getSize que recoge un Path y devuelve un long con el tamaño.
Se necesita el método auxiliar getSize() porque Files.size() declara IOException, y prefiero no poner un bloque try/catch dentro de una expresión lambda. Podemos imprimir los datos usando el método format():
Dependiendo del directorio en el que ejecutes esto, imprimirá algo como esto:
Tamaño total del árbol de directorios: 15.30 megabytes
4.03. Aplicación de un límite de profundidad
Digamos que nuestro árbol de directorios es bastante profundo, así que aplicamos un límite de profundidad cambiando una línea de código en nuestro método getPathSize().
try (var s = Files.walk(origen, 5)) {
Esta versión sobrecargada verifica los archivos sólo dentro de 5 pasos del nodo inicial. Un valor de profundidad de 0 indica la propia ruta actual. Dado que el método calcula valores sólo en archivos, se tendrá que asignar un límite de profundidad de al menos 1 para obtener un resultado distinto de cero cuando se aplica este método a un árbol de directorios.
4.04. Evitar rutas circulares: NOFOLLOW_LINKS
Muchos de los métodos anteriores de NIO.2 recorren enlaces simbólicos por defecto, con un NOFOLLOW_LINKS utilizado para desactivar este comportamiento. El método walk() se comporta de modo diferente porque no sigue enlaces simbólicos por defecto y requiere que se habilite la opción FOLLOW_LINKS. Podemos alterar método el anterior getPathSize() para habilitar el seguimiento de enlaces simbólicos agregando la opción FileVisitOption:
try (var s = Files.walk(source, FileVisitOption.FOLLOW_LINKS)) {
Al recorrer un árbol de directorios, el programa debe tener cuidado con los enlaces simbólicos si están habilitados. Por ejemplo, si nuestro proceso se encuentra con un enlace simbólico que apunta al directorio raíz del sistema de archivos, ¡entonces se buscarían todos los archivos en el sistema!
Peor aún, un enlace simbólico podría llevar a un ciclo, en el que una ruta se visita repetidamente. Un ciclo es una dependencia circular infinita en la que una entrada en un árbol de directorios apunta a uno de sus directorios ancestrales. Digamos que tenemos un árbol de directorios como se muestra en imagen siguiente, con el enlace simbólico: /usuario/pepe/todos que apunta a /usuario. Podemos observar el sistema de archivos con ciclo:
¿Qué sucede si intentamos recorrer este árbol y seguir todos los enlaces simbólicos, comenzando con /usuario/pepe? La siguiente tabla muestra las rutas visitadas después de caminar una profundidad de 3. Para simplificar, caminaremos por el árbol en un orden de búsqueda en amplitud, aunque un ciclo ocurre independientemente de la estrategia de búsqueda utilizada. Caminar un directorio con un ciclo usando búsqueda en amplitud:
Después de caminar una distancia de 1 desde el inicio, alcanzamos el enlace simbólico /usuario/pepe/todos y volvemos a la parte superior del árbol de directorios /usuario. Eso está bien porque aún no hemos visitado /usuario, ¡así que aún no hay un ciclo! Por desghracia, en la profundidad 2, encontramos un ciclo, pues ya se ha visitado el directorio /usuario/pepe en nuestro primer paso, y ahora nos estamos encontrando con él nuevamente. Si el proceso continúa, estaremos condenados a visitar el directorio una y otra vez.
Excepción FileSystemLoopException cuando se visita más de una vez.
Cuando se usa la opción FOLLOW_LINKS, el método walk() realizará un seguimiento de todas las rutas que ha visitado, lanzando una FileSystemLoopException si una ruta se visita dos veces.
5. Buscar un directorio con find()
En el ejemplo anterior, aplicamos un filtro al objeto Stream<Path> para filtrar los resultados, aunque NIO.2 proporciona un método más conveniente.
El método find() se comporta de manera similar al método walk(), excepto que toma un BiPredicate para filtrar los datos. También requiere que se establezca un límite de profundidad. Al igual que walk(), find() también admite la opción FOLLOW_LINK.
Nota: esta interface funcional, @FunctionalInterface public interface BiPredicate<T,U>, dispone un método de comprobación: boolean test(T t, U u), que evalúa un predicado con los dos argumentos recogidos. Devuelve true si los argumentos se ajustan al predicado.
Los dos parámetros del BiPredicate son un objeto Path y un objeto BasicFileAttributes. De esta manera, NIO.2 recupera automáticamente la información básica del archivo, lo que permite escribir expresiones lambda complejas que tienen acceso directo a este objeto (la fecha de creación, modificación o acceso, si es un directorio o un archivo regular, si es un enlace simbólico, su tamaño,…). Por ejemplo:
Path path = Paths.get("/coles");
long tamanhoMin = 1_000;
try (var s = Files.find(path, 10,
(p, a) -> a.isRegularFile() && p.toString()
.endsWith(".java") && a.size() > tamanhoMin)) {
s.forEach(System.out::println);
}
Este ejemplo busca un árbol de directorios e imprime todos los archivos .java con un tamaño de al menos 1,000 bytes, utilizando un límite de profundidad de 10. Aunque podríamos haber logrado esto usando el método walk() junto con una llamada a readAttributes(), esta implementación es mucho más corta y conveniente. Además, no tenemos que preocuparnos de que los métodos dentro de la expresión lambda lancen una excepción verificada, como en el ejemplo de getPathSize().
6. Leer el contenido de un archivo con lines()
Hemos visto cómo leer el contenido de un archivo con Files.readAllLines(), que devuelve una lista de String, y comentamos que usarlo para leer un archivo muy grande podría resultar en un problema de OutOfMemoryError:
El contenido del archivo se lee y procesa de forma perezosa (lazy), lo que significa que sólo se almacena en memoria una pequeña porción del archivo en un momento dado.
Este código de muestra busca y muestra del archivo las líneas que comiencen con CORO:, imprimiendo el texto que sigue. Suponiendo que el archivo de entrada sharks.log es el siguiente:
Como puedes ver, la programación funcional en NIO.2 nos da la capacidad de manipular archivos de maneras complejas, a menudo sólo unas pocas expresiones cortas.
6. Files.readAllLines() vs. Files.lines()
Necesitas conocer la diferencia entre readAllLines() y lines(). Ambos de estos ejemplos se compilan y ejecutan:
La primera línea lee todo el archivo en memoria y realiza una operación de impresión sobre el resultado, mientras que la segunda línea procesa perezosamente cada línea e imprime a medida que se lee. La ventaja del segundo fragmento de código es que no requiere que todo el archivo se almacene en memoria en ningún momento.
También debes tener en cuidado cuando se mezclan tipos incompatibles. ¿Ves por qué lo siguiente no compila?
La respuesta es que el método filter() espera un Predicate, y el método readAllLines() devuelve una List<String>. Los dos tipos no son compatibles, por lo que no se puede utilizar un método en el otro sin alguna forma de conversión.
Ahora bien, una código similar que compila es la siguiente:
Esto se debe a que lines() devuelve un Stream<String>, y filter() espera un > Predicate<String>. Ambos comparten el mismo tipo genérico, por lo que el código compila sin problemas. Esto es un recordatorio importante de que las lambdas y los métodos de referencia deben coincidir exactamente con la firma del método funcional correspondiente. En este caso, la firma del método funcional es Predicate<String>, que coincide con la firma de filter().
7. Comparación de java.io.File y NIO.2
I/O File
Método NIO.2
file.delete()
Files.delete(path)
file.exists()
Files.exists(path)
file.getAbsolutePath()
path.toAbsolutePath()
file.getName()
path.getFileName()
file.getParent()
path.getParent()
file.isDirectory()
Files.isDirectory(path)
file.isFile()
Files.isRegularFile(path)
file.lastModified()
Files.getLastModifiedTime(path)
file.length()
Files.size(path)
file.listFiles()
Files.list(path)
file.mkdir()
Files.createDirectory(path)
file.mkdirs()
Files.createDirectories(path)
file.renameTo(otherFile)
Files.move(path,otherPath)
Un gran número de métodos de NIO.2 no están disponibles en java IO, como soporte para enlaces simbólicos, asignacion de atributos del distema, y más. Java NIO.2 es una biblioteca más avanzada y poderosa que la tradicional java.io.File.
Implementación de un parser (analizador) JSON propio.
JSON es la abreviatura de JavaScript Object Notation.
JSON es un formato de intercambio de datos popular entre navegadores y servidores web porque los navegadores pueden analizar JSON en objetos JavaScript de forma nativa.
En el servidor, sin embargo, JSON debe analizarse y generarse mediante las API de JSON. Este apartado estudiaremos las diversas opciones que tiene para Java analizar y generar JSON.
JSON (JavaScript Object Notation) es un formato de datos independiente del lenguaje que expresa objetos JSON como listas de propiedades (pares de nombre/valor) fácilmente legibles.
Nota: JSON permite que el separador de línea Unicode U+2028 y el separador de párrafo U+2029 aparezcan sin escapar en cadenas entre comillas. Dado que JavaScript no admite esta característica, JSON no es un subconjunto adecuado de JavaScript.
JSON se utiliza normalmente, entras, para:
La comunicación asincrónica entre el navegador y el servidor a través de AJAX (Ajax).
En sistemas de gestión de bases de datos NoSQL como MongoDb y CouchDb.
En aplicaciones de sitios web de redes sociales como Twitter, Facebook, LinkedIn y Flickr; e incluso con la API de Google Maps.
Nota: Muchos desarrolladores prefieren JSON sobre XML porque consideran que JSON es menos extenso y más fácil de leer. Consulta “JSON: la alternativa baja en calorías a XML” JSON: The Fat-Free Alternative to XML para obtener más información.
Veremos cuáles son las API JSON que existen en Java (no están incluidas en JDK), así como trabajar con archivos JSON en Java en general.
JSON significa: JavaScript Object Notation (Notación de Objetos de JavaScript).
Es un formato para estructurar datos. Este formato es utilizado por diferentes aplicaciones web para comunicarse entre sí.
JSON es un formato de intercambio de datos popular entre navegadores y servidores web porque los navegadores pueden analizar JSON en objetos JavaScript de forma nativa.
En el servidor, sin embargo, JSON debe analizarse y generarse mediante las API de JSON.
JSON es un formato de datos independiente del lenguaje que expresa objetos JSON como listas legibles por humanos de propiedades (pares de nombre/valor).
Nota: JSON permite que el separador de línea Unicode U+2028 y el separador de párrafo U+2029 aparezcan sin escapar en cadenas entre comillas. Dado que JavaScript no admite esta característica, JSON no es un subconjunto adecuado de JavaScript.
JSON se utiliza normalmente para la comunicación asincrónica entre el navegador y el servidor a través de AJAX (Ajax).
También se utiliza:
En Sistemas de gestión de bases de datos NoSQL como MongoDb y CouchDb.
En aplicaciones de sitios web de redes sociales como Twitter, Facebook, LinkedIn y Flickr
Incluso con la API de Google Maps.
Podría decirse que es el sustituto del formato de intercambio de datos XML:
Es fácil estructurar los datos en comparación con XML.
Admite estructuras de datos como arrays y objetos.
Los documentos JSON se ejecutan rápidamente en el servidor o en cualquier lenguaje que disponga de biblioteca correspondiente.
La sintaxis de JSON procede de la notación de objetos de JavaScript, pero el formato de JSON es sólo texto.
La generación y lectura de JSON existe para muchos lenguajes, que suelen disponer de bibliotecas para hacerlo.
Nota: Muchos desarrolladores prefieren JSON sobre XML porque consideran que JSON es menos extenso y más fácil de leer. Consulta “JSON: la alternativa baja en calorías a XML” (JSON: The Fat-Free Alternative to XML para obtener más información.
Veremos cuales son las API JSON existen en Java (no están incluidas en JDK), así como trabajar con archivos JSON en Java en general.
2. Características
Es un formato independiente del lenguaje que se deriva de JavaScript.
Es legible y escribible por humanos, ya que es un formato de texto plano utilizando la notación de objetos de JavaScript.
Es un formato de intercambio de datos basado en texto y ligero, lo que significa que es más sencillo de leer y escribir en comparación con XML.
Aunque se deriva de un subconjunto de JavaScript, es independiente del lenguaje. Por lo tanto, el código para generar y analizar datos JSON se puede escribir en cualquier otro lenguaje de programación, como Java.
Transmisión de Datos entre Computadoras: JSON se utiliza para enviar datos entre computadoras y programas.
3. Reglas sintácticas
Los datos están organizados en pares de nombre/valor separados por comas. Utiliza llaves para contener los objetos { } y corchetes [ ] para contener los arrays.
JSON presenta un objeto JSON como una lista delimitada por llaves y separada por comas de propiedades (una coma no aparece después de la última propiedad):
{
propiedad1,
propiedad2,
...
propiedadN
}
Para cada propiedad, el nombre se expresa como una cadena que generalmente está entre comillas dobles. La cadena del nombre se sigue por dos puntos, que a su vez es seguido por un valor de un tipo específico. Ejemplos incluyen "nombre": "Otto" y "edad": 4.
JSON admite los siguientes seis tipos, que veremos más adelante:
Cadena: una secuencia de cero o más caracteres Unicode. Las cadenas están delimitadas por comillas dobles y admiten una sintaxis de escape con barra invertida.
Número: un número decimal (en base 10) que puede contener una parte fraccional y puede usar notación exponencial (E).
Booleano: Cualquiera de los valores true o false.
Array: una lista ordenada de cero o más valores, cada uno de los cuales puede ser de cualquier tipo. Los arrays utilizan la notación de corchetes cuadrados con elementos separados por comas.
Objeto: una colección no ordenada de propiedades donde los nombres (también llamados claves) son cadenas. Dado que los objetos están destinados a representar arrays asociativos, se recomienda, aunque no es obligatorio, que cada clave sea única dentro de un objeto. Los objetos están delimitados por llaves y usan comas para separar cada propiedad. Dentro de cada propiedad, los dos puntos separan la clave de su valor.
Nulo: Un valor vacío, utilizando la palabra clave null.
La sintaxis JSON es un subconjunto de la sintaxis de JavaScript.
La sintaxis JSON se deriva de la sintaxis de la notación de objetos de JavaScript:
Los datos están en pares de nombre/valor.
Los datos están separados por comas.
Las llaves ({}) contienen objetos.
Los corchetes ([]) contienen arrays.
3.2. Datos JSON - “clave”: valor
Los datos JSON se escriben como pares de nombre/valor (también conocidos como pares clave/valor).
Un par de nombre/valor consiste en un nombre de campo (entre comillas dobles), seguido de dos puntos y luego un valor.
Ejemplo
"nombre":"Otto"
Los nombres JSON requieren comillas dobles.
3.2. JSON - se evalúa como objetos de JavaScript
El formato JSON es casi idéntico a los objetos de JavaScript.
En JSON, las claves deben ser cadenas, escritas entre comillas dobles.
JSON:
{"nombre": "Otto"}
4. Ventajas de JSON
Almacena todos los datos en un array para que la transferencia de datos sea más fácil. Es la mejor opción para compartir datos de cualquier tamaño, incluso audio, video, etc.
Su sintaxis es muy pequeña, fácil y liviana, por lo que ejecuta y responde de manera más rápida.
Tiene un amplio rango de compatibilidad con el navegador y es compatible con los sistemas operativos. No requiere mucho esfuerzo para hacerlo compatible con todos los navegadores.
En el lado del servidor, el análisis es la parte más importante que los desarrolladores desean. Si el análisis es rápido en el lado del servidor, el usuario puede obtener una respuesta rápida, por lo que en este caso, el análisis del lado del servidor de JSON es un punto fuerte en comparación con otros.
5. Desventajas de JSON
La principal desventaja es que no hay manejo y gestión de errores. Si hay un pequeño error en el script, no se obtendrán datos estructurados.
Se vuelve bastante peligroso cuando se usa con algunos navegadores no autorizados. Como el servicio JSON devuelve un archivo JSON envuelto en una llamada a función que debe ser ejecutada por los navegadores, si los navegadores no están autorizados, tus/los datos pueden ser hackeados.
Tiene herramientas con soporte limitado que podemos usar durante el desarrollo.
6. Tipos de datos JSON
JSON (JavaScript Object Notation) es el formato de datos más ampliamente utilizado para el intercambio de datos en la web. JSON es un formato de intercambio de datos basado en texto y completamente independiente del lenguaje. Se basa en un subconjunto del lenguaje de programación JavaScript y es fácil de entender y generar.
6.1. Tipos de datos JSON
En JSON, los valores deben ser uno de los siguientes tipos de datos:
Una cadena (string)
Un número (number)
Un objeto (object)
Un array (array)
Un booleano (boolean)
null
A diferencia, en JavaScript, los valores pueden ser todos los anteriores, además de cualquier otra expresión JavaScript válida, incluyendo:
Una función (function)
Una fecha (date)
undefined
JSON admite principalmente 6 tipos de datos:
String (Cadena)
Las cadenas JSON deben escribirse entre comillas dobles, al igual que en el lenguaje Java o C.
En JSON, los valores de tipo cadena deben escribirse entre comillas dobles:
Ejemplo
{"nome":"Wittgenstein"}
Hay varios caracteres especiales (caracteres de escape) en JSON que se pueden usar en cadenas, como \ (barra invertida), / (barra diagonal), b (retroceso), n (nueva línea), r (retorno de carro), t (tabulación horizontal), etc.
Aquí \/ se utiliza como caracter de escape para / (barra diagonal).
Number (Número)
Se representa en base 10 y no se utilizan formatos octales ni hexadecimales.
Un número decimal firmado que puede contener una parte fraccional y puede usar notación exponencial (E).
JSON no permite NotANumber (como NaN), no hace distinción entre enteros y punto flotante. Además, como he comentado anteriormente JSON no reconoce los formatos octal y hexadecimal. (Aunque JavaScript utiliza un formato de punto flotante de doble precisión para todos los valores numéricos, otros lenguajes que implementan JSON pueden codificar los números de manera diferente).
Ejemplo:
{ "edad": 32 }
{ "calificación": 9.5 }
Boolean (Booleano)
Este tipo de datos puede ser verdadero (true) o falso (false).
Ejemplo:
{ "premioPulitzer": true }
Null (Nulo)
Es simplemente un valor nulo definido.
Ejemplo
{
"premioNobel": null,
"publicaciones": 25 }
Object (Objeto)
Es un conjunto de pares de nombre o valor insertados entre {} (llaves). Las claves deben ser cadenas y deben ser únicas. Múltiples pares de claves y valores se separan por una coma (,).
Dado que los objetos están destinados a representar arrays asociativos, se recomienda, aunque no es obligatorio, que cada clave sea única dentro de un objeto. Los objetos están delimitados por llaves y usan comas para separar cada propiedad. Dentro de cada propiedad, los dos puntos separan la clave de su valor.
Es una colección ordenada de cero o más valores y comienza con [ (corchete izquierdo) y termina con ] (corchete derecho). Los valores del array están separados por , (coma).
Sintaxis:
[ valor, .......]
Ejemplo:
{
"obras": ["Ariel", "The Bell Jar", "Colossus"]
}
1. Ejemplo de JSON con el API de Java (Scripting API)
En teoría, JSON no está en la API estándar de Java. Sin embargo, podremos hacerlo con Java’s Scripting API.
Nota: En 2014, Oracle presentó una Propuesta de Mejora de Java (JEP) para agregar una API de JSON a Java. Aunque “JEP 198: Light-Weight JSON API”, http://openjdk.java.net/jeps/198, se actualizó en 2017, probablemente pasarán varios años antes de que esta API de JSON se convierta en parte de Java.
En el siguiente ejemplo, sólo a modo de muestra, podemos usar JavaScript, pero en un contexto de Java mediante la API de Scripting de Java. (No te preocupes, no será demandado, pero es importante saber que existe). El siguiente código fuente Java permite ejecutar código JavaScript:
import java.io.FileReader;
import java.io.IOException;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import static java.lang.System.*;
publicclassRunJSScript {
publicstaticvoidmain(String[] args) {
if (args.length!= 1) {
err.println("uso: java RunJSScript scriptEnJS");
return;
}
ScriptEngineManager manager =new ScriptEngineManager(); // Inicio el API de Scripting ScriptEngine engine = manager.getEngineByName("nashorn");
try {
engine.eval(new FileReader(args[0])); // Sí, los flujos con importantes } catch (ScriptException se) {
err.println(se.getMessage());
} catch (IOException ioe) {
err.println(ioe.getMessage());
}
}
}
Ojo: en versiones actuales de Java quizás debas añadir un motor de JavaScript a tu proyecto Maven, como ECMAScript como el proporcionado por Oracle GraalVMOracle GraalVM for JDK 21
El método main anterior verifica primero que se haya especificado exactamente un argumento desde línea de órdenes, que es el nombre de un archivo de script. Si no es así, muestra información de uso y termina el programa. Por ello, debe recoger como argumento un programa/script en JavaScript, por ejemplo:
Suponiendo que se indicó un sólo argumento de línea de órdenes, se instancia la clase javax.script.ScriptEngineManager. ScriptEngineManager sirve como punto de entrada en la API de Scripting.
A continuación, se llama al método ScriptEngine getEngineByName(String shortName) del objeto ScriptEngineManager para obtener un motor de script correspondiente al valor deseado de shortName. Java 11 admite el motor de script nashorn (aunque ha sido obsoleto), que devuelve como un objeto cuya clase implementa la interfaz javax.script.ScriptEngine.
ScriptEngine declara varios métodos eval() para evaluar un script. main() invoca el método Object eval(Reader reader) para leer el script desde su objeto java.io.FileReader y (asumiendo que no se arroje java.io.IOException) luego evalúa el script. Este método devuelve cualquier valor de retorno del script, que ignoro. Además, este método arroja javax.script.ScriptException cuando ocurre un error en el script.
Compila:
javac RunJSScript.java
Suponiendo el Script se llama poeta.js, ejecuta la aplicación de la siguiente manera:
java RunJSScript poeta.js
Deberías observar la siguiente salida (junto con un mensaje de advertencia sobre la eliminación planeada de Nashorn en una futura versión de JDK):
Sylvia
Plath
New York
646 555-4567
2. Parser de JSON: JSON.parse()
Un objeto JSON existe como texto independiente del lenguaje.
Para convertir el texto en un objeto dependiente del lenguaje, necesitas analizar el texto.
JavaScript proporciona un objeto JSON con un método parse() para esta tarea. Pasa el texto a analizar como argumento a parse() y recibe el objeto basado en JavaScript resultante como el valor de retorno de este método. parse() lanza una SyntaxError cuando el texto no se ajusta al formato JSON.
Suponiendo que el Script anterior se encuentra en tarjeta.js, ejecuta la aplicación de la siguiente manera:
java RunJSScript tarjeta.js
Deberías observar la siguiente salida:
1234567890123456
20/04
visa
SyntaxError: JSON no válido: <json>:1:2 Se esperaba , o } pero se encontró '
{ 'type': 'visa' }
^ en <eval> en la línea número 11
El error de sintaxis muestra que no puedes delimitar un nombre con comillas simples (solo las comillas dobles son válidas).
Ejercicio 2: lectura de datos de un archivo JSON con Java Script API
Clasificación de la Liga de Baloncesto ACB
A partir del documento JSON anterior, copia el archivo JSON en un documento JavaScript para proceder a su lectura y que devuelva los datos del Obradoiro, suponiendo que es el primero de la lista.
Emplea el método eval que recoge un objeto de tipo Reader. Usa una clase con buffer creada con la API de Java NIO.2.
Como hemos comentado, JSON es la abreviatura de JavaScript Object Notation, un formato de intercambio de datos popular entre navegadores y servidores web porque los navegadores pueden analizar JSON en objetos JavaScript de forma nativa, es un formato de datos independiente del lenguaje que expresa objetos JSON como listas legibles por humanos de propiedades (pares de nombre/valor).
Sin embargo, aunque los navegadores puedan analizarlos mediante JavaScript, en el servidor (y en programación cliente) JSON debe analizarse y generarse mediante las API de JSON. Como se ha comentado anteriormente, JSON se utiliza normalmente para la comunicación asíncrona entre el navegador y el servidor a través de AJAX (Ajax).
Este apartado veremos algunas de las muchas opciones que tiene para Java analizar y generar JSON.
Separadores de línea
Nota: JSON permite que el separador de línea Unicode U+2028 y el separador de párrafo U+2029 aparezcan sin escapar en cadenas entre comillas. Dado que JavaScript no admite esta característica, JSON no es un subconjunto adecuado de JavaScript.
Además, también es ampliamente utilizado en:
En Sistemas de gestión de bases de datos NoSQL como MongoDb y CouchDb.
En aplicaciones de sitios web de redes sociales como Twitter, Facebook, LinkedIn y Flickr.
Incluso con la API de Google Maps.
¿JSON o XML?
Nota: Muchos desarrolladores prefieren JSON sobre XML porque consideran que JSON es menos extenso y más fácil de leer. Consulta “JSON: la alternativa baja en calorías a XML” (JSON: The Fat-Free Alternative to XML para obtener más información.
Trabajar con datos JSON en Java puede ser relativamente sencillo, pero, como casi todo en Java, hay muchas opciones y bibliotecas entre las que podemos elegir.
Veremos algunas API JSON existen de Java para JSON (que no están incluidas en JDK), así como trabajar con archivos JSON en Java en general.
2. APIs de JSON en Java
Cuando se popularizó el formato JSON, Java no tenía una implementación estándar de analizador/generador JSON, javax.json.bind. Por ello han surgido varias implementaciones de API de JSON de código abierto para Java.
Desde entonces, Java ha intentado abordar la API JSON de Java que falta en JSR 353, que no es un estándar oficial (de momento).
La comunidad Java también ha desarrollado varias API Java JSON de código abierto. Las API JSON de Java de código abierto a menudo ofrecen más opciones y flexibilidad en la forma en que puede trabajar con JSON que la API JSR 353. Por lo tanto, las API de código abierto siguen siendo opciones decentes (y mejores).
Algunas de las API Java JSON de código abierto más conocidas son:
Hasta hace poco Jackson era el ganador, pero en la actualidad GSON es probablemente el más completo y uno de los más rápidos (en las pruebas que he comprobado para pequeños proyectos), seguido de cerca por JSONP/JSONB, Jackson y luego JSON.simple en último lugar (no aparece Boon ni JSON.org en este análisis, ni las implementaciones de JSON-P y JSON-B).
Existen también bibliotecas de alto rendimiento como dsl-json o la de Alibaba (China), rápidas y de alta implantación.
A modo de curiosidad, la siguiente tabla se muestran ejemplos de los resultados porcentuales que he encontrado, pero dicha evaluación probablemente haya quedado en anticuada:
Velocidad de parsing
MB/ms
Tiempo de parsing
GSON
100%
0%
Jackson
58%
70.87%
JSON.simple
79%
126.58%
JSONP
44%
25.49%
En ella, GSON es un claro ganador, aunque con reservas.
1. GSON
GSON es una API Java JSON de Google, de ahí viene la G en GSON. GSON es razonablemente flexible, hasta hace poco, Jackson era más rápido que GSON. Pero hoy en día el rendimiento de GSON supera muchas alternativas:
Jackson es una API Java JSON que proporciona varias formas diferentes de trabajar con JSON. Jackson es una de las API Java JSON más populares que existen. La página inicial de Jackson es la siguiente:
publicvoidserializaDeserializaJackson()
throws IOException{
// Creación del objeto: Alumno objeto =new Alumno(4,"Otto");
// Mapeador ObjectMapper mapper =new ObjectMapper();
// Conversión en JSON (serialización): String jsonStr = mapper.writeValueAsString(objeto); // Cadena JSON// Lectura de objeto JSON: Alumno alumno = mapper.readValue(jsonStr, Alumno.class); // Deserialización}
La cadena será algo como (depende de las propieddades de la clase Alumno):
{
"edad":4,
"nombre":"Otto"}
3. JSONP: Jakarta JSON Processing
JSONP es API JSON compatible compatible con JSR 374 significa que si utiliza las API estándar, debería ser posible intercambiar la implementación de JSONP con otra API en el futuro, sin cambiar el código. Puedes encontrar información JSONP en el repositorio y en la página oficial:
JSON-P proporciona una API de Java para procesar datos con formato JSON a más bajo nivel que JSON-P, por lo que, en algún caso, puede ser más sencillo trabajar con la nuevaAPI JSON-B, que con poco código nos permite generar y procesar archivos JSON.
4. JSON-P y JSON-B (Java API for JSON Binding)
La especificación JSON-B proporciona una capa de enlace sobre JSON-P, lo que simplifica aún más la conversión de objetos hacia y desde JSON (más sencillo ;-))
Propósito: JSON-P proporciona una API para procesar (analizar y generar) documentos JSON. Está diseñada para ser una solución de bajo nivel y se centra principalmente en proporcionar un modelo de objeto JSON (similar a un árbol) y una forma de navegar y manipular ese modelo.
Características:
Ofrece dos modelos: Object Model (similar a un árbol) y Streaming API (procesamiento basado en eventos).
Se utiliza para analizar documentos JSON en una estructura de objetos Java (JsonObject, JsonArray, etc.).
Puede usarse para generar documentos JSON a partir de objetos Java.
Forma parte de la especificación Java EE (Enterprise Edition), pero también es aplicable en entornos Java SE (Standard Edition).
4.2. Java API for JSON Binding (JSON-B)
Propósito: JSON-B se centra en la serialización y deserialización automática entre objetos Java y JSON. Su objetivo principal es simplificar la tarea de convertir objetos Java en notación JSON y viceversa, eliminando la necesidad de escribir manualmente código de conversión.
Características:
Define un conjunto de anotaciones (@JsonbProperty, @JsonbTransient, etc.) para personalizar el mapeo entre los objetos Java y JSON.
Permite la personalización a través de adaptadores y estrategias.
No proporciona un modelo de objeto JSON como JSON-P, ya que su enfoque es más alto nivel, centrado en la conversión entre objetos Java y JSON.
Es parte de las especificaciones de Java EE y también está disponible para aplicaciones Java SE.
JSON-P es más general y se utiliza para el procesamiento directo de JSON, mientras que JSON-B se especializa en la serialización y deserialización de objetos Java a y desde JSON.
¿Cuál es mejor?
JSON-B es la API preferida para convertir objetos Java hacia y desde JSON, gracias a su seguridad de tipos, facilidad de uso y comentarios en tiempo de compilación. Sin embargo, en algunos casos, JSON-P podría ser más adecuado.
Ejemplo de JSON-P
Dependencia Maven JSON-P:
<!-- (Más actual) Versión Jakarta: Jakarta JSON Processing defines a Java(R) based framework for parsing, generating, transforming, and querying JSON documents --><dependency><groupId>jakarta.json</groupId><artifactId>jakarta.json-api</artifactId><version>2.1.2</version></dependency><dependency><groupId>org.glassfish</groupId><artifactId>jakarta.json</artifactId><version>2.0.1</version></dependency>
<!-- (Más antiguo) --><dependency><groupId>javax.json</groupId><artifactId>javax.json-api</artifactId><version>1.1</version></dependency><dependency><groupId>org.glassfish</groupId><artifactId>javax.json</artifactId><version>1.1</version></dependency>
<!-- (Más actual) Versión Jakarta: Jakarta JSON Processing defines a Java(R) based framework for parsing, generating, transforming, and querying JSON documents --><dependency><groupId>jakarta.json.bind</groupId><artifactId>jakarta.json.bind-api</artifactId><version>3.0.0</version></dependency><dependency><groupId>org.eclipse</groupId><artifactId>yasson</artifactId><version>3.0.3</version></dependency>
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
publicclassJsonBExemplo {
publicstaticvoidmain(String[] args) {
// Crear un objeto Java Persona persona =new Persona("Otto", 4, "Santiago de Compostela");
// Crear un objeto Jsonb Jsonb jsonb = JsonbBuilder.create();
// Convertir el objeto Java a JSON String strJson = jsonb.toJson(persona);
// Imprimir la cadena JSON System.out.println("JSON Resultante (JSON-B):");
System.out.println(strJson);
}
// Clase de ejemplostaticclassPersona {
String nome;
int idade;
String cidade;
publicPersona(String nome, int idade, String cidade) {
this.nome= nome;
this.idade= idade;
this.cidade= cidade;
}
}
}
En ellos puede verse la creación y conversión de objetos JSON usando JSON-P y JSON-B. Por supuesto,deben añadirse las bibliotecas correspondientes en tu proyecto para ejecutar estos ejemplos, como javax.json-api para JSON-P y javax.json.bind-api y org.eclipse.yasson para JSON-B.
Ejercicio con JSON-B
Crea un proyecto Maven con una sencilla clase Examen que contenga los siguientes atributos:
materia: de tipo String.
fecha: de tipo LocalDateTime.
participantes: de tipo List de String con los nombres de los estudiantes.
Crea los métodos get/set que consideres adecuados, así como un método toString() que devuelva la materia, la fecha seguida de la lista de participantes (emplea StringBuilder).
Crea una sencilla aplicación que cree un examen de “Acceso a Datos” para el 12 de noviembre del 2023 a las 9:45 horas, con 5 estudiantes con nombres de poetas femeninas del siglo XX.
Guarda el examen en un archivo JSON llamado accesoADatos.json mediante el api de JSON-B y muestre el contenido del archivo por pantalla, utilizando Files de Java NIO.2 y recupere el archivo para guardarlo en un nuevo objeto Java.
JSON.org también tiene una API Java JSON de código abierto. Esta fue una de las primeras API Java JSON disponibles. Es razonablemente fácil de usar, pero no tan flexible o rápido como las otras API JSON mencionadas anteriormente.
Como también dice el repositorio de Github, ésta es una antigua API Java JSON. No recomiendo su uso a menos que el proyecto ya lo esté usando. De lo contrario, busca una de las otras opciones más actualizadas, preferiblemente GSON o Jackson.
5. mJson (descontinuado)
mJson es una pequeña biblioteca Java para JSON (creada por el desarrollador Borislav Lordanov) que se utiliza para analizar objetos JSON en objetos Java y viceversa. Esta biblioteca está documentada en GitHub (http://bolerio.github.io/mjson/, y presenta las siguientes características:
Soporte completo para la validación de JSON Schema Draft 4.
Un único tipo universal: todo es un objeto Json; no hay conversión de tipos.
Un único método de tipo Factory para convertir un objeto Java en un objeto Json; simplemente llama a Json.make(cualquier objeto Java aquí).
Análisis rápido y codificado a mano.
Diseñado como una estructura de datos de propósito general para su uso en Java.
Punteros de padre y método up() para recorrer la estructura JSON.
Métodos concisos para leer (Json.at()), modificar (Json.set(), Json.add()), duplicar (Json.dup()), y fusionar (Json.with()).
Fusión flexible de estructuras profundas Deep-merging.
Métodos para la verificación de tipos (por ejemplo, Json.isString()) y acceso al valor subyacente de Java (por ejemplo, Json.asString())
Encadenamiento de métodos
Factory adaptable para construir tu propio soporte para el mapeo arbitrario entre Java y JSON
Biblioteca completa ubicada en un archivo Java, sin dependencias externas.
A diferencia de otras bibliotecas JSON, mJson se centra en la manipulación de estructuras JSON en Java sin asignarlas a objetos Java fuertemente tipados. Como resultado, mJson reduce la la escritura de código y permite trabajar con JSON en Java tan sencillo como en JavaScript.
6. Boon (descontinuado)
Boon es una API Java JSON menos conocida, pero supuestamente es (era) la más rápida de todas (según el último benchmark que he podido comprobar).
Boon se está utilizando como la API JSON estándar en Groovy.
Repositorio:
La API de Boon es muy similar a la de Jackson (por lo que es fácil de cambiar). Pero Boon es más que una API Java JSON. Boon es un kit de herramientas de propósito general para trabajar con datos fácilmente. Esto es útil, por ejemplo, dentro de los servicios REST, aplicaciones de procesamiento de archivos, etc.
Boon contiene los siguientes analizadores Java JSON:
El Boon ObjectMapper que puede analizar JSON en objetos personalizados o mapas Java
Al igual que en Jackson, Boon ObjectMapper también se puede utilizar para generar JSON a partir de objetos Java personalizados.
GSON es el analizador (parser) y generador JSON de Google para Java. Google desarrolló GSON para uso interno, pero lo abrió más tarde. GSON es razonablemente fácil de usar. En este apartado veremos cómo usar GSON para analizar objetos JSON en Java y serializar objetos Java en JSON.
GSON contiene varias clases del API que se pueden usar para trabajar con JSON. En principio, nos centraremos en los componentes de GSON que analiza documentos JSON en en objetos Java o genera JSON a partir de objetos Java:
La clase Gson que puede analizar objetos JSON en objetos Java personalizados y viceversa, a traves de los métodos fromJSon y toJson, respectivamente.
El GSON JsonReader, que es el analizador JSON de flujos de GSON, que analiza un token JSON a la vez.
El GSON JsonParser que puede analizar JSON en una estructura de árbol de objetos Java específicos de GSON.
Para utilizar GSON en la aplicación Java es necesario incluir el archivo GSON JAR en la ruta de clases de su aplicación Java.
También puede hacerse agregando GSON como una dependencia de Maven a su proyecto, o descargando el archivo JAR e incluirlo en la ruta de clase manualmente:
API Javadoc, documentacion de la versión más reciente.
2. Gson: convertir objetos Java a JSON y viceversa
Gson es una biblioteca de Java que se puede utilizar para convertir objetos Java en su representación JSON. También se puede utilizar para convertir una cadena JSON en un objeto Java equivalente.
Gson puede trabajar con objetos Java arbitrarios, incluidos los objetos preexistentes de los que no tiene el código fuente.
Existen algunos proyectos de código abierto que pueden convertir objetos Java a JSON, como los que hemos visto en el apartado anterior. Sin embargo, la mayoría de las apis de JSON requieren que coloque anotaciones de Java en sus clases, algo que no puede hacer si no tiene acceso al código fuente. Además, la mayoría de ellos no admiten completamente el uso de genéricos de Java.
Gson considera ambos como objetivos de diseño muy importantes y no precisa anotaciones y permite genéricos.
3. Características de Gson
Proporciona métodos toJson() y fromJson() simples para convertir objetos Java a JSON y viceversa.
Permite la conversión de objetos preexistentes y que no se puedan modificar a y desde JSON.
Amplio soporte de genéricos de Java.
Permite representaciones personalizadas para objetos.
Admite objetos arbitrariamente complejos (con jerarquías de herencia profundas y uso extensivo de tipos genéricos).
Gson se distribuye como un único archivo JAR; gson-2.10.1.jar es el archivo JAR más reciente, ahora. Para conseguir el archivo JAR, puedes ir al repositorio Maven este enlace, clic en el enlace de descargas y selecciona “jar” del menú desplegable, luego guarda el archivo gson-2.10.1.jar cuando se te pida hacerlo. Además, es posible que desees descargar gson-2.10.1-javadoc.jar, que contiene la documentación de esta API.
Es fácil trabajar con gson-2.10.1.jar. Simplemente inclúyelo en el CLASSPATH al compilar el código fuente o al ejecutar una aplicación, de la siguiente manera:
A pesar de admitir versiones antiguas de Java, Gson también proporciona un descriptor de módulo JPMS (nombre del módulo: com.google.gson) para usuarios de Java 9 o posterior.
Dependencias de JPMS (Java 9+)
Estos son los módulos opcionales del Sistema de Módulos de Plataforma Java (JPMS) en los que Gson depende.
Esto sólo se aplica al ejecutar Java 9 o posterior.
java.sql (opcional desde Gson 2.8.9): Cuando este módulo está presente, Gson proporciona adaptadores predeterminados para algunas clases de fecha y hora SQL.
jdk.unsupported, respectivamente, la clase sun.misc.Unsafe (opcional): Cuando este módulo está presente, Gson puede utilizar la clase Unsafe para crear instancias de clases sin constructor sin argumentos (sin constructor por defecto). Sin embargo, hay que tener cuidado al depender de esto. Unsafe no está disponible en todos los entornos y su uso tiene algunas trampas; consulta GsonBuilder.disableJdkUnsafe().
Nivel mínimo de API de Android
Gson 2.11.0 y posterior: API nivel 21
Gson 2.10.1 y anteriores: API nivel 19
Es posible que versiones antiguas de Gson también admitan niveles de API más bajos, aunque esto no se ha verificado.
com.google.gson: este paquete proporciona acceso a Gson, la clase principal para trabajar con Gson.
com.google.gson.annotations: este paquete proporciona tipos de anotaciones para su uso con Gson.
com.google.gson.reflect: este paquete proporciona una clase de utilidad para obtener información de tipo de un tipo genérico.
com.google.gson.stream: este paquete proporciona clases de utilidad para leer y escribir valores codificados en JSON.
Empezaremos con la clase Gson, hablaremos de la deserialización de Gson (analizando objetos JSON), seguido por la serialización de Gson (creando objetos JSON).
Terminaremos discutiendo brevemente características adicionales de Gson, como anotaciones y adaptadores de tipo.
La clase Gson gestiona la conversión entre JSON y objetos Java.
Se puede crear instancias de esta clase utilizando el constructor Gson(), o ppor medio de la clase com.google.gson.GsonBuilder.
El siguiente fragmento de código demuestra ambos enfoques:
Como norma general, usa Gson() cuando se desee trabajar con la configuración predeterminada (en la mayoría de los casos), y utiliza GsonBuilder cuando se quiera anular la configuración predeterminada.
Las llamadas a los métodos de configuración se encadenan, y el método create() de GsonBuilder se llama al final para devolver el objeto Gson resultante.
2. Creación de una instancia de Gson
Antes de poder usar GSON, primero debe crearse un nuevo objeto Gson. Hay dos formas de crear una instancia de Gson:
Usando el new Gson()
Crear una instancia de GsonBuilder e invocar al método create() en ella.
2.1. Creación con new Gson()
Puede crearse un objeto Gson simplemente creándolo con la orden: new Gson();. Así es como se ve la creación de un objeto Gson:
Gson gson =new Gson();
Una vez que haya creado una instancia de Gson, puede comenzar a usarla para analizar y generar JSON.
2.2. Creación con GsonBuilder.create()
Otra forma de crear una instancia de Gson es crear un objeto de tipo builder GsonBuilder() y llamar a su método create(). Por ejemplo:
El uso de un GsonBuilder es más flexible, ya que permite añadir opciones de configuración en GsonBuilder antes de crear el objeto Gson.
2.3. Configuración predeterminada (que puede cambiarse en GsonBuidler)
Gson admite la siguiente configuración predeterminada (la lista no está completa; consulta la documentación de Gson y GsonBuilder para obtener más información):
Gson proporciona serialización y deserialización predeterminadas para clases comunes del API, como instancias de java.lang.Enum, java.util.Map, java.net.URL, java.net.URI, java.util.Locale, java.util.Date,java.math.BigDecimal y java.math.BigInteger. Se puede cambiar la representación predeterminada registrando un adaptador de tipo (Lo veremos más adelante) a través de GsonBuilder.registerTypeAdapter(Type, Object).
El texto JSON generado omite todos los campos nulos. Sin embargo, conserva los nulos en los arrays porque un array es una lista ordenada. Además, si un campo no es nulo pero su texto JSON generado está vacío, se conserva el campo. Se configura Gson para serializar valores nulos llamando a GsonBuilder.serializeNulls().
El formato de fecha predeterminado es el mismo que java.text.DateFormat.DEFAULT. Este formato ignora la parte de milisegundos de la fecha durante la serialización. Se puede cambiar el formato predeterminado invocando GsonBuilder.setDateFormat(int) o GsonBuilder.setDateFormat(String).
La política predeterminada de nombrado de atributos para el formato JSON de salida es la misma que en Java. Por ejemplo, un campo de clase Java llamado versionNumber se mostrará como “versionNumber” en JSON. Las mismas reglas se aplican al mapear JSON entrante a clases Java. Se puede cambiar esta política llamando a GsonBuilder.setFieldNamingPolicy(FieldNamingPolicy).
El texto JSON generado por los métodos toJson() se representa de manera compacta: se eliminan todos los espacios en blanco innecesarios. Se puede cambiar este comportamiento llamando a GsonBuilder.setPrettyPrinting().
Por defecto, Gson ignora las anotaciones @Since (lo veremos más adelante, para serializar sólo campos después desde determinadas versions). Puedes habilitar a Gson para que utilice estas anotaciones llamando a GsonBuilder.setVersion(double).
Por defecto, Gson ignora las anotaciones @Expose (serialice o no el atributo). Puedes habilitar a Gson para que serialice/deserialize solo aquellos campos marcados con esta anotación llamando a GsonBuilder.excludeFieldsWithoutExposeAnnotation().
Por defecto, Gson excluye campos transitorios (transient) o estáticos de la consideración para la serialización y deserialización. Puedes cambiar este comportamiento llamando a GsonBuilder.excludeFieldsWithModifiers(int...).
3. Conversión entre primitivas JSON y sus equivalentes Java: fromJson() y toJson()
Una vez que tienes un objeto Gson, se puede invocar a los métodos fromJson() y toJson() para convertir entre JSON y objetos Java, respectivamente. Por ejemplo, código siguiente presenta una aplicación sencilla que obtiene un par de objetos Gson y demuestra la conversión entre JSON y objetos Java en términos de primitivas JSON.
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import static java.lang.System.*;
publicclassGsonDemo {
publicstaticvoidmain(String[] args) {
Gson gson =new Gson();
// Deserialization de una cadena String nome = gson.fromJson("\"Sylvia Plath\"", String.class);
out.println(nome);
// Serializacion de un entero gson.toJson(256, out); // por pantalla out.println(); // salto de línea.// Serialización gson.toJson("<html>", out); // por pantalla. out.println(); // salto de línea// Gson personalizado deshabilitando el escapado de HTML gson =new GsonBuilder().disableHtmlEscaping().create();
gson.toJson("<html>", out); // Sin escapar HTML out.println();
}
}
Ejercicio. Conversión de primitivas JSON
Crea un proyecto y compila el código anterior. Comprueba el resultado.
Explicación:
El listado anterior declara una clase GsonDemo cuyo método main() primero instancia Gson, manteniendo su configuración predeterminada. Luego, invoca el método genérico <T> T fromJson(String json, Class<T> classOfT) de Gson para deserializar el texto JSON especificado (en json), basado en java.lang.String, en un objeto de la clase especificada (classOfT), que en este caso es String.
La cadena JSON “Sylvia Plath” (las comillas dobles son obligatorias), que se expresa como un objeto String de Java, se convierte (sin las comillas dobles) en un objeto String de Java. Una referencia a este objeto se asigna a nome.
Después de imprimir el nombre devuelto, main() llama al método void toJson(Object src, Appendable writer) de Gson para convertir el entero (en clase envolvente) 256 (almacenado por el compilador en un objeto java.lang.Integer) en un entero JSON y mostrar el resultado en la salida estándar.
main() vuelve a invocar toJson() para mostrar una cadena de Java que contiene <html>. Por defecto, Gson escapa los caracteres HTML < y >, por lo que estos caracteres no se imprimen. Para evitar este escape, es necesario obtener un objeto Gson a través de GsonBuilder, invocando el método disableHtmlEscaping() de GsonBuilder, que hace main() a continuación. Un segundo intento de imprimir <html> revela que no hay escape.
GSON puede generar JSON a partir de objetos Java empleando un objeto Gson (y viceversa).
Para generar JSON, invocamos al método toJson() del objeto Gson.
Ejemplo:
Poeta poeta =new Poeta();
poeta.setNome("Sylvia Plath");
poeta.setIdade(30);
Gson gson =new Gson();
String json = gson.toJson(poeta);
1.1. Impresión con formato “elegante”: .setPrettyPrinting()
Por defecto, la instancia Gson creada con new Gson()imprime (genera) JSON de la forma más compacta posible (¡El carácter espacio o un salto de línea, por ejemplo, ocupan espacio!. En transferencia de datos hay que economizar, sobre todo cuando se transfieren muchos archivos).
La salida compacta JSON predeterminada de Gson:
{"nome":"Sylvia Plath","idade":30}
Sin embargo, este JSON compacto puede ser difícil de leer. Por lo que GSON ofrece una opción de “impresión bonita” donde el JSON se imprime de manera que sea más legible en un editor de texto: por medio del método setPrettyPrinting() de GsonBuilder
Para crear una instancia de Gson con la opción de impresión bonita habilitada se crea por medio de la clase GsonBuilder:
Un ejemplo de cómo se vería el mismo JSON con impresión bonita:
{
"nome": "Sylvia Plath",
"idade": 30}
2. De JSON a Java: fromJson()
GSON puede convertir JSON en objetos Java utilizando el método fromJson() del objeto Gson. Ejemplo de GSON parseando JSON en un objeto Java:
String textoJson ="{\"nome\":\"Sylvia Plath\", \"idade\": 30}"; // Cadena JSON a analizarGson gson =new Gson();
Poeta poeta = gson.fromJson(textoJson, Poeta.class); // Debemos indicar el tipo de objeto a crear
Pasos del ejemplo:
Creamos la cadena JSON a analizar.
Creamos la instancia de Gson.
Invocamos al método gson.fromJson(), que analiza la cadena JSON en un objeto Poeta, la versión que recoge una cadena:
El primer parámetro de fromJson() es la fuente JSON (String, Reader, JsonReader o JsonElement).
En el ejemplo anterior, la fuente JSON es una cadena, pero existen varias versiones de este método (sobrecargado).
El segundo parámetro del método fromJson() es la clase de Java para analizar el JSON en una instancia.
La instancia Gson crea un objeto de esta clase y analiza el JSON en él. Por lo tanto, debes asegurarte de que esta clase tenga un constructor sin argumentos, o GSON no podrá usarla.
Crea un proyecto Maven, igual que el anterior con JSON-B pero con GSON, con la sencilla clase Examen que contiene los siguientes atributos:
materia: de tipo String.
fecha: de tipo Date, no LocalDateTime. (Veremos por qué, pero puedes hacer una prueba con LocalDateTime).
participantes: de tipo List de String con los nombres de los estudiantes.
Crea los métodos get/set que consideres adecuados, así como un método toString() que devuelva la materia, la fecha seguida de la lista de participantes (emplea StringBuilder).
Crea una sencilla aplicación que cree un examen de “Acceso a Datos” para el 12 de noviembre del 2024 a las 9:45 horas, con 5 estudiantes con nombres de poetas femeninas del siglo XX.
_NOTA: para pasar de LocalDate a Date puedes emplear la sentencia:
Guarda el examen en una archivo JSON llamado accesoADatos.json (de manera “vistosa” y con formato de fecha yyyy-MM-dd HH:mm) mediante el api de Gson y muestre el contenido del archivo por pantalla, utilizando Files de Java NIO.2 y recupere el archivo para guardarlo en un nuevo objeto Java.
Crea una clase ClasificacionDAO para guardar la clasificación de equipos de baloncesto con dos atributos privados y estáticos con los nombres de los archivos para leer y guardar la clasificación:
OBJECT_FILE: con el nombre de fichero clasificacion.dat para guardar el objeto Java como un flujo a objeto.
JSON_FILE: con el nombre de fichero clasificacion.json para guardar el objeto Java en formato JSON.
Además, debe tener un atributo privado, gson, de tipo Gson para trabajar con JSON.
El constructor por defecto debe crear ese objeto de tipo Gson, pero de modo que tenga una escritura legible.
La clase debe tener 6 métodos:
saveToObject(Clasificacion c): que guarda la clasificación en el fichero OBJECT_FILE. Emplea Java NIO.2 para crear el flujo de tipo Buffered.
saveToJSON(Clasificacion c, String file): que guarda la clasificación en el fichero recogido como argumento. Emplea el objeto de tipo Gson y Java NIO.2 para guardar la cadena, a ser posible en una línea. La escritura debe tener un formato legible (no en una línea de texto).
saveToJSON(Clasificacion c): que guarda la clasificación en el fichero JSON_FILE. Emplea un objeto de tipo Gson y Java NIO.2 para guardar la cadena, a ser posible en una línea. Puedes llamar al método anterior.
getFromObject(): que obtiene la clasificación a partir del fichero OBJECT_FILE. Emplea Java NIO.2 para crear el flujo de tipo Buffered.
getFromJSON(String file): que obtiene la clasificación a partir del fichero recogido como argumento. Emplea Java NIO.2
getFromJSON(): que obtiene la clasificación a partir del fichero JSON_FILE. Invoca al método anterior.
3. Exclusión de atributos en la serialización
Con GSON puede indicarse que excluya atributos de tus clases Java durante la serialización.
Existen varias formas de decirle a GSON que excluya un campo. Veremos algunas:
3.1. Atributos transient
Como hemos visto en la parte de flujos, cuando marcamos un atributo como transient no se enviará al flujo.
GSON ignora los atributos marcados como transient tanto en la serialización como en la deserialización. Así es como se ve la clase Poeta que usamos en el primer ejemplo, con el campo “nome” marcado como transient:
publicclassPoeta {
publictransient String nome =null; // no se serializapublicint idade;
}
La anotación @Expose de GSON (com.google.gson.annotations.Expose) se puede usar para marcar un atributo para que se exponga o no (se incluya o no) al serializar o deserializar un objeto.
La anotación @Exposepuede tener dos parámetros: serialize y deserialize, ambos son booleanos que pueden tener los valores true o false:
El parámetro serialize de la anotación @Expose indica si el atributo anotado con la @Expose debe incluirse cuando el objeto se serializa.
El parámetro deserialize anota si ese atributo debe leerse cuando el objeto se deserializa.
Por ejemplo, la anotación @Expose:
@Expose(serialize = true);
@Expose(serialize = false);
@Expose(deserialize = true);
@Expose(deserialize = false);
@Expose(serialize = true, deserialize = false);
@Expose(serialize = false, deserialize = true);
Ejemplos de clase que utiliza la anotación @Expose:
publicclassEstudiante {
@Exposeprivate String nome; // Se incluirá en la serialización y deserialización@Expose(serialize =false) private String apelidos;
@Expose (serialize =false, deserialize =false) private String email; // noprivate String password; // ... ? NO LO SERIALIZA NI DESERIALIZA }
@Expose en objetos creados con new Gson()
Si se crea un objeto Gson con new Gson(), los métodos toJson() y fromJson() utilizarán los atributos del objeto para la serialización y deserialización (en el ejemplo anterior, nome, apelidos, email y password). Sin embargo, si se generó el objeto Gson con Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() los métodos toJson() y fromJson() de Gson excluirán el atributo password. Esto se debe a que el atributo password, que no está marcado con la anotación @Expose.
Gson también excluirá apelidos e email de la serialización ya que serialize está configurado en false. De manera similar, Gson excluirá email la deserialización ya que deserialize está configurado en false.
publicclassPoeta {
@Expose(serialize =false, deserialize =false)
public String nome =null;
@Expose(serialize =true, deserialize =true)
publicint idade = 31;
}
Observa la anotación @Expose sobre los atributos, indicando si el campo dado debe incluirse al serializar o deserializar.
Para que GSON tenga en cuenta a las anotaciones @Expose, se debe crear una instancia de Gson utilizando la clase GsonBuilder. Así es cómo se ve eso:
Ten en cuenta que esta configuración hace que GSON ignore todos los atributos que no tengan una anotación @Expose. Para que un campo se incluya en la serialización o deserialización, debe tener una anotación @Expose sobre él.
3.3 Exclusión de campos con GsonBuilder.setExclusionStrategies()
Otra forma de excluir un campo de una clase de la serialización o deserialización en GSON es usar GsonBuilder para construir el objeto Gson y configurar una ExclusionStrategy en un GsonBuilder.
Dentro del método shouldSkipField() de la implementación de ExclusionStrategy, en el ejemplo, verifica si el nombre de campo dado es “password”. Si es así, ese campo se excluye de la serialización y deserialización.
Para usar la implementación de ExclusionStrategy, se crea un GsonBuilder y establece la ExclusionStrategy en él usando el método setExclusionStrategies(), de la siguiente manera:
La variable politicaExclusion debe apuntar a una implementación de la interfaz ExclusionStrategy.
El objeto de tipo FieldAttributes tiene métodos para obtener el nombre del campo, la clase que lo declara, el tipo declarado, si tiene modificador o las anotaciones que tiene el campo. Eso nos permite hacer filtros más dinámicos combinando esos métodos.
Clase FieldAttributes
La interfaz ExclusionStrategy tiene dos versiones sobrecargadas del método shouldSkipField, una con el parámetro de tipo Class y tomando un objeto de tipo FieldAttributes como parámetro.
Un objeto de tipo FieldAttributes tiene método para obtener el nombre ()getName()), su valor como cadena (toString()), la clase que lo declara (getDeclaredClass()), el tipo declarado (getDeclaredType()), si tiene modificador (hasModifier (int modifier)) o las anotaciones que tiene el campo (getAnnotations()). Eso nos permite hacer filtros más dinámicos combinando esos métodos.
3.4. Serialización de Campos Nulos
Por defecto, el objeto Gson no serializa campos con valores nulos a JSON. Si un campo en un objeto Java es nulo, Gson lo excluye.
Se puede obligar a serializar a Gson valores nulos a través de GsonBuilder. Por ejemplo:
GsonBuilder builder =new GsonBuilder();
builder.serializeNulls(); // esto es lo que vemos ahora.Gson gson = builder.create();
Materia ad =new Materia();
ad.nome=null;
String json = gson.toJson(ad);
System.out.println(json);
Una vez que se ha llamado a serializeNulls(), la instancia de Gson creada por GsonBuilder incluirá campos nulos en el JSON serializado.
La salida del ejemplo anterior sería:
{"nome":null,"horas":9, "profesor": "javhoz"}
Observa cómo el campo nome es nulo.
Gestión de equipos y clasificaciones con archivos JSON
Se trata de completar la tarea de Clasificación de equipos con archivos JSON, creando clases DAO que trabajen con archivos JSON.
Haga un programa para la gestión y clasificación de las ligas, como la ACB. Las clasificaciones de los equipos se guardan en archivos binarios o de texto, según decidas. Por ejemplo: Liga ACB.json.
a) Declare una clase Equipo con los atributos mínimos necesarios: nombre, victorias, derrotas, puntosAfavor a favor, puntosEnContra puntos en contra. Puedes añadir los atributos que te interesen, como ciudad, etc. Tienes libertad para hacerlo, pues, además, te puede servir como práctica. En una liga de fútbol, por ejemplo, se podría añadir el campo estadio y los puntos a favor serían los goles a favor.
Además, ten en cuenta que los atributos puntos, partidos jugados y diferencia de puntos son atributos derivados que se calculan a partir de los partidos ganados, perdidos, puntos a favor y puntos en contra.
Cree los métodos que considere oportunos, pero tome decisiones sobre los métodos get/set necesarios. Así, haz un método que devuelva los puntos, getPuntos, un método getPartidosJugados que devuelva el número de partidos jugados y un método getDiferenciaDePuntos, que devuelva la diferencia de puntos. Obviamente, por ser atributos/propiedades derivados/as, no tienen sentido los métodos de tipo “set” para ellos.
Debe tener, al menos, un constructor para la clase equipo que recoja el nombre y otro que recoja todas las propiedades. Debe existir un constructor por defecto.
Para poder ordenar los equipos debe implantar la interface Comparable<Equipo>. Piense que debe ordenar por puntos y, a igualdad de puntos, por diferencia de puntos encestados. Además, debe implantar la interfaz Serializable. Lo mismo con la clase siguiente, Clasificacion, que debe implementar la interfaz Serializable.
Sobrescribe el método equals para que se considere que dos Equipos son iguales si tienen el mismo nombre (sin distinguir mayúsculas de minúsculas). Haz lo mismo con hashCode.
b) Declare una clase Clasificacion, con los atributos:
equipos de tipo Set de Equipo (será de tipo TreeSet), aunque debe existir un constructor que permita crear una clasificación con los equipos que se desee.
competicion de tipo String que recoja el nombre de la competición. Por defecto, la competición debe ser “Liga ACB”.
Defina los métodos para añadir equipos a la clasificación, addEquipo, así como los métodos para eliminar equipo, removeEquipo, y sobrescriba el método toString que devuelva la cadena de la clasificación (StringBuilder)
Los constructores de Clasificación deben crear el conjunto de equipos como tipo TreeSet, para que los ordene automáticamente.
c) Interface DAO<T, K> (Data Access Object) es un patrón de diseño que permite separar la lógica de negocio de la lógica de acceso a los datos. Con los siguientes métodos:
import java.util.List;
/**
* Dao genérico.
* Esta clase define los métodos que deben implementar las clases que quieran
* ser un Dao.
* La T es el tipo de objeto que se va a manejar y la K es el tipo de clave
* primaria.
* @param <T>
* @param <K>
*/publicinterfaceDao<T, K> {
T get(K id);
List<T>getAll();
booleansave(T obxecto);
booleandelete(T obx);
booleandeleteAll();
booleandeleteById(K id);
voidupdate(T obx);
}
e) Crea una clase EquipoJSONDAO que implemente la interfaz DAO<Equipo, String>. Debe implantar los métodos de la interfaz.
Esta clase debe tener un atributo final, path, de tipo Path con la ruta completa al archivo de datos JSON en el que se guarda la clasificación completa.
f) Cree una clase ClasificacionJSONeDAO que implemente la interfaz DAO<Clasificacion, String>.
Debe tener un atributo final con la ruta en la que se guardan los datos de la clasificación: ruta. El nombre del archivo debe ser el nombre de la competición seguido de .json.
Constructor al que se le pasa la ruta, etc.
Para facilitar el trabajo. los métodos de la clase ClasificacionFileDAO pueden hacer uso de la clase EquipoFileDAO.
g) Haz las pruebas necesarias para comprobar el correcto funcionamiento.
Como mejora, intenta hacerlo con una aplicación gráfica.
A ser posible emplea el patrón Factory para crear los objetos DAO:
// Ejemplo de Factory generalpublicclassDaoFactory {
publicstatic Dao getDao(String tipo){
if(tipo.equalsIgnoreCase("equipo")){
returnnew EquipoJSONDAO();
} elseif(tipo.equalsIgnoreCase("clasificacion")){
returnnew ClasificacionJSONDAO();
}
returnnull;
}
}
// Ejemplo de Factory para ClasificaciónpublicclassClasificacionDAOFactoy {
publicstatic Dao<Clasificacion, String>getClasificacionDAO(String tipo) {
if (tipo.equalsIgnoreCase("file")) {
return ClasificacionFileDAO.getInstance();
} elseif (tipo.equalsIgnoreCase("json")) {
return ClasificacionFileDAO.getInstance();
} else{
returnnull;
}
}
}
Ejercicio: serialización JSON del Sudoku
A partir del ejercicio de la tarea del Sudoku, y por medio de las dos estrategias vistas anteriormente, haz que no serialice en un archivo JSON el alfabero del Sudoku y sólo lo haga con los datos. Además, debe escribirlo de manera “legible”.
Crea una clase SudokuDAO con las siguientes características:
JSON_FILE: con el nombre de fichero sudoku.json para guardar el objeto Java en formato JSON.
Además, debe tener un atributo privado, gson, de tipo Gson para la trabajar con JSON.
El constructor por defecto debe crear ese objeto de tipo Gson, pero de modo que tenga una escritura legible.
La clase debe tener los siguientes métodos:
Para trabajar con objetos Java:
saveToObject(Sudoku c, String ruta): que guarda el sudoku en el fichero recogido como argumento. Emplea Java NIO.2 para crear el flujo de tipo Buffered. ¿Cuál es la diferencia entre Files.newOutputStream() y new FileOutputStream()?
getFromObject(String ruta): recoge la ruta al fichero y de devuelve el objeto guardado en dicho fichero mediante el método anterior. Emplea Java NIO.2 para crear el flujo de tipo Buffered.
Para trabajar con objetos JSON:
saveToJSON(Sudoku c, String file): que guarda el sudoku en el fichero recogido como argumento. Emplea el objeto de tipo Gson y Java NIO.2 para guardar la cadena, a ser posible en una línea de código. La escritura debe tener un formato legible (no en una línea de texto).
saveToJSON(Sudoku c): que guarda el sudoku en el fichero JSON_FILE. Emplea un objeto de tipo Gson y Java NIO.2 para guardar la cadena, a ser posible en una línea. Puedes llamar al método anterior.
getFromJSON(String file): que obtiene el sudoku a partir del fichero recogido como argumento. Emplea Java NIO.2
getFromJSON(): que obtiene el sudoku a partir del fichero JSON_FILE. Invoca al método anterior.
Pata trabajar con archivos de texto:
Sudoku getFromTXT(String ruta): lee el sudoku de un archivo de texto en el que cada línea son los caracteres de cada fila y devuelve el sudoku equivalente.
Reto: crea un método que resuelva el sudoku.
A modo de ejemplo, puedes ver este código que imprime la soluciones por pantalla.
publicvoidresolver() throws Exception {
// Los hijos de cada Sudoku List<Sudoku> hijos = getChildren();
for (Sudoku hijo : hijos) {
if (hijo.isValid() && hijo.isCompleted()) {
System.out.println("Solución:");
System.out.println(hijo);
} elseif (hijo.isValid()) {
hijo.resolver();
}
}
}
Crea un atributo para guardar las soluciones, List<Sudoku> solucions;, y crea el atributo en el constructor (mejor).
Crea un método get para las soluciones.
Haz que el método resolver() guarde las soluciones hijo en la lista de soluciones.
Implanta un método en SudokuDAO que implante un método para guardar las soluciones en un archivo JSON: saveSolutionsToJSON(String ruta).
01.06. Gson. Transformación de objetos JSON personalizada
public GsonBuilder registerTypeAdapter (Type tipo, Object tipoDeAdaptador)
Que se emplea para la serialización o deserialización personalizada.
Este método puede registrar varios tipos de adaptadores:
Adaptadores de tipo: TypeAdapter, clase abstracta empleada para personalizar la adaptación de tipos integrados, implantando los métodos write (JsonWriter out, T valor) y read(JsonReader reader):
El método write se emplea para serializar un objeto de tipo T en un JSON: public abstract void write (JsonWriter out, T value) throws IOException. Por ejemplo, para serializar un objeto de tipo Persona:
publicvoidwrite(JsonWriter out, Persona persona) throws IOException {
if(out ==null|| persona ==null) {
writer.nullValue();
return;
}
out.beginObject();
out.name("nombre").value(persona.getNombre());
out.name("edad").value(persona.getEdad());
out.endObject();
}
Los métodos principales de JsonWriter son: beginArray(), endArray(), beginObject(), endObject(), name(String name), value(String value), value(Boolean value), value(double value), value(long value), nullValue(), setLenient(boolean lenient), setIndent(String indent), setSerializeNulls(boolean serializeNulls), close(), flush(). El método name(String name) se emplea para escribir el nombre de un atributo en un objeto JSON y el método value(T value) para escribir el valor de un atributo en un objeto JSON.
El método read se emplea para deserializar un JSON en un objeto de tipo T: public abstract T read(JsonReader in) throws IOException. Por ejemplo, para deserializar un objeto de tipo Persona:
public Persona read(JsonReader in) throws IOException {
if(in ==null) {
returnnull;
}
Persona persona =new Persona();
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case"nombre":
persona.setNombre(in.nextString());
break;
case"edad":
persona.setEdad(in.nextInt());
break;
default:
in.skipValue();
break;
}
}
in.endObject();
return persona;
}
Los métodos principales de JsonReader son: beginArray(), endArray(), beginObject(), endObject(), hasNext(), nextName(), nextString(), nextBoolean(), nextDouble(), nextInt(), skipValue(), setLenient(boolean lenient), close(), peek(), skipValue(). El método nextName() se emplea para leer el nombre de un atributo en un objeto JSON y los métodos nextString(), nextBoolean(), nextDouble(), nextInt() para leer el valor de un atributo en un objeto JSON. Se puede usar el método peek() para ver el siguiente token sin consumirlo y tomar decisiones::
Creadores de instancia: InstanceCreator<T>, interfaz que debe implantarse para crear instancias de una clase sin constructor por defecto. Siempre, si fuese posible, es mejor implantar un constructor por defecto.
Serialización y deserialización personalizada: JsonSerializer<T> y un JsonDeserializer<T>. Interfaces que representa un serializador y deserializador personalizado para JSON. Debe escribir un serializador/deserializador personalizado si no estás satisfecho con la serialización predeterminada realizada por Gson.
Se utiliza mejor cuando un único objeto TypeAdapter implementa todas las interfaces necesarias para la serialización personalizada con Gson. Si se registró previamente un adaptador de tipo para el tipo especificado, este será sobrescrito.
Este método registra solo el tipo especificado y ningún otro: ¡debes registrar manualmente los tipos relacionados! Por ejemplo, las aplicaciones que registran boolean.class también deben registrar Boolean.class.
JsonSerializer y JsonDeserializer son “a prueba de nulos”. Esto significa que al intentar serializar null, Gson escribirá un JSON null y no se llamará al serializador. De manera similar, al deserializar un JSON null, Gson emitirá null sin llamar al deserializador. Si se desea manejar valores nulos, en su lugar, se debe usar un TypeAdapter.
public GsonBuilder registerTypeAdapter (Type type, Object typeAdapter) {
// Implementación del método// ...returnthis; // Devuelve una referencia a este objeto GsonBuilder}
Empezaremos viendo cómo restringir la serialización y deserialización por versión de la clase y pasaremos a ver ejemplos de adaptadores personalizados.
1. Soporte de versiones en GSON: @Since y @Until
GSON permite un control sencillo de versiones de las clases para los objetos Java que lee y escribe. El soporte de versiones en GSON significa que se pueden marcar los atributos de las clases Java con un número de versión y luego hacer que GSON incluya o excluya campos de tus clases Java según su número de versión.
Estas anotaciones son útiles para gestionar el control de versiones de las clases JSON.
@Until indica el número de versión HASTA que un miembro o un tipo debe estar presente. Si Gson se crea con un número de versión igual o superior al valor almacenado de la anotación @Until, el campo se ignorará en la salida JSON.
@Since indica el número de versión DESDE que un miembro o un tipo debe estar presente.
(1) Ejemplo de la clase Persoa con sus campos anotados con las anotaciones @Since y @Until:
import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;
publicclassPersoa {
// He puesto los atributos como públicos para simplificar// el código, pero no se recomienda en absoluto.@Since(1.0)
public String nome =null;
@Since(1.0)
public String apelidos =null;
@Until(2.0)
public String cidade =null;
@Since(3.0)
public String email =null;
}
(2) En segundo lugar, debes crear un GsonBuilder y decirle a qué versión (desde o hasta) debería serializar y deserializar.
Por ejemplo:
GsonBuilder builder =new GsonBuilder();
// Versión 2.0, entonces serializa/deserializa // los que tienen un @Since menor o igual a 2.0 o @Until // mayor que 2.0 builder.setVersion(2.0);
Gson gson = builder.create();
La instancia de Gson creada a partir del GsonBuilder anterior solo incluirá campos que estén anotados con @Since(2.0) o con un número de versión inferior a 2.0, así como los campos que tengan @Until superior a 2.0 (no inclúido).
En el ejemplo de la clase Persoa anterior los campos nome y apelidos serán incluidos, no así cidade porque tiene un valor igual (no superior) a 2.0. El campo email está anotado con la versión 3.0, que es posterior a 2.0, por lo que GSON también excluirá el campo email.
Ejemplo de cómo serializar un objeto Persoa a JSON y ver el JSON generado:
Observa cómo GSON excluyó el campo email y cidade en el JSON generado.
Excluir campos basados en la versión funciona de la misma manera para leer JSON en objetos Java (deserialización). Observa la siguiente cadena JSON que contiene todos los campos, incluido el campo email:
Si se intenta leer un objeto Persoa con la instancia de Gson anterior, el campo email y el campo cidade no se leerán incluso si está presente en la cadena JSON.
Así se vería la lectura de un objeto Persoa con la instancia de Gson anterior:
String persoaJson2 ="{\"nome\":\"Anne\",\"apelidos\":\"Sexton\",\"cidade\":\"Santiago\",\"email\":\"anne@doe.com\"}";
Person persoaLeida = gson.fromJson(persoaJson2, Persoa.class);
Comprueba el resultado.
2. Creación de objetos personalizados en GSON: InstanceCreator
GSON de manera prederminada crea los objetos a partir de un JSON invocando al constructor por defecto.
En muchos casos la clase no tiene un constructor predeterminado, o se desea realizar alguna configuración predeterminada del objeto, o se desea crear una instancia de una subclase en su lugar.
Para eso Gson tiene una interface: com.google.gson.InstanceCreator.
import com.google.gson.InstanceCreator;
publicclassCreadorDePoetasimplements InstanceCreator<Poeta> {
public Poeta createInstance(Type tipo) {
Poeta poeta =new Poeta();
poeta.setCategoria("Poesía");
return poeta;
}
}
Se puede usar la clase CreadorDePoetas registrándola en un GsonBuilder con el método registerTypeAdapter antes de crear la instantcia de tipo Gson: gsonBuilder.registerTypeAdapter(Poeta.class, new CreadorDePoetas());
El objeto de tipo Gson del ejemplo anterior utilizará la instancia CreadorDePoetas para crear objetos de tipo Poeta.
Comprúebalo con el siguiente código (haciendo uso del código anterior):
String poetaJson ="{ \"nome\" : \"Anne Sexton\", \"idade\" : 45}";
Poeta poeta = gson.fromJson(poetaJson, Poeta.class);
// se supone que poeta tiene un campo denominado categoria.System.out.println(poeta.getCategoria());
El valor predeterminado de la propiedad categoria es nulo y la cadena JSON no contiene una propiedad categoria. Sin embargo, se asigna el valor para la propiedad categoria establecido dentro del método createInstance() de CreadorDePoetas (Poesía).
3. Serialización y Deserialización personalizados: JsonSerializer y JsonDeserializer
GSON ofrece la posibilidad de utilizar serializadores y deserializadores personalizados.
Los serializadores personalizados pueden convertir valores Java a JSON personalizado, y los deserializadores personalizados pueden convertir JSON personalizado de nuevo a valores Java.
3.1. Serializador personalizado
Un serializador personalizado en GSON debe implementar la interfaz funcional JsonSerializer. La interfaz JsonSerializer:
publicinterfaceJsonSerializer<T> {
public JsonElement serialize(T valor, Type tipo,
JsonSerializationContext jsonSerializationContext) {
}
}
JSONElement es una clase abstracta que representa un elemento JSON. Subclases de JSONElement son:
JsonArray: representa un array JSON, Podemos añadir elementos a un JsonArray con el método add() y obtener un elemento con el método get(int i). También es posible obtener el array como un único elemento Java si contiene un único elemento: getAsBoolean(), getAsCharacter(), getAsDouble(), getAsFloat(), getAsInt(), getAsString(), etc.
JsonObject: representa un objeto JSON. Podemos añadir elementos a un JsonObject con el método add(String property, JsonElement value) o addProperty(String property, T value) y obtener un elemento con el método get(String nombreMiembro) o como Array, Objeto y tipo primitivo con los métodos getAsJsonArray(), getAsJsonObject(), getAsJsonPrimitive().
JsonPrimitive, que son Boolean, Character, Number o String y permite crear un JSON primitivo: new JsonPrimitive(1), new JsonPrimitive("Wittgenstein"), new JsonPrimitive(true), new JsonPrimitive('a').
Por ejemplo, para declarar un serializador personalizado que pueda serializar valores booleanos:
publicclassBooleanSerializerimplements JsonSerializer<Boolean> {
public JsonElement serialize(Boolean aBoolean, Type tipo,
JsonSerializationContext jsonSerializationContext) {
if(aBoolean){
returnnew JsonPrimitive(1);
}
returnnew JsonPrimitive(0);
}
}
Observa cómo el parámetro de tipo T se sustituye con la clase Boolean en dos lugares.
Dentro del método serialize(), puedes convertir el valor (un Boolean en este caso) a un JsonElement, que el método serialize() debe devolver. En el ejemplo anterior, utilizamos un JsonPrimitive, que también es un JsonElement. Como puedes ver, los valores booleanos verdaderos se convierten en 1 y los falsos en 0, en lugar de true y false normalmente usados en JSON.
JsonElement
Existen 4 subclases de JsonElement que pueden ser devueltas: JsonArray, JsonNull.INSTANCE, JsonObject, JsonPrimitive, que son Boolean, Character, Number o String.
Ten en cuenta que el método serialize devuelve un objeto de tipo JsonElement.
Una vez registrado, la instancia de Gson creada a partir de GsonBuilder utilizará el serializador personalizado. Para ver cómo funciona, utilizaremos la siguiente clase POJO:
publicclassUsuario {
public String usuario =null;
public Boolean esSuperUsuario =false;
}
Así es como se ve la serialización de una instancia de Usuario:
Observa cómo el valor false de esSuperUsuario se convierte en un 0.
3.2. Deserializador personalizado
GSON también permite deserializadores personalizados.
Un deserializador personalizado debe implementar la interfaz JsonDeserializer.
Debe escribir un deserializador personalizado si se quiere modificar la deserialización predeterminada realizada por Gson. Además, también se debe registrar el deserializador a través de GsonBuilder.registerTypeAdapter(Type, Object).
La interfaz JsonDeserializer:
publicinterfaceJsonDeserializer<T> {
public Boolean deserialize(JsonElement jsonElement,
Type tipo, JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException;
}
Implementar un deserializador personalizado para el tipo Boolean se vería así:
La salida impresa de este ejemplo de deserialización personalizada de GSON sería:
true
… ya que el 1 en la cadena JSON se convertiría en el valor booleano true.
Ejemplo avanzado
Veamos un ejemplo más avanzado en dónde la serialización y deserialización resulta más útil. La clase Id definida a continuación tiene dos campos: clase y valor.
La deserialización predeterminada de Id(com.otto.MiClase.class, 20L) requerirá que la cadena JSON sea {"clase":"com.otto.MiClase","valor":20}.
Supongamos que se conoce el tipo del campo en el que se deserializará el Id y, por lo tanto, sólo se desea deserializarlo a partir de una cadena JSON 20.
Se puede hacer escribiendo un deserializador personalizado:
publicclassIdDeserializerimplements JsonDeserializer<Id> {
public Id deserialize(JsonElement json, Type tipoDeT, JsonDeserializationContext context)
throws JsonParseException {
long idValor = json.getAsJsonPrimitive().getAsLong();
returnnew Id((Class) tipoDeT, idValor);
}
}
También se debe registrar el objeto de tipo IdDeserializer con Gson:
Gson gson =new GsonBuilder().registerTypeAdapter(Id.class, new IdDeserializer()).create();
TypeAdapter o JsonSerializer/JsonDeserializer
Las nuevas aplicaciones deberían emplear TypeAdapter, incorporado en la versión 2.1 de Gson, cuya API de transmisión es más eficiente que la API de las interfaces JsonDeserializer<T> y JsonSerializer<T>, pues no requiere la creación de objetos intermedios y emplea flujo de salida y entrada de JSON directamente, con menor consumo de memoria.
Ejercicio. Clase Examen con JsonSerializer y JsonDeserializer de LocalDateTime
Modifica la clase Examen que contiene los siguientes atributos:
materia: de tipo String.
fecha: LocalDateTime, ahora LocalDateTime.
participantes: de tipo List de String con los nombres de los estudiantes.
Para que la fecha la guarde en formato LocalDateTime, no Date.
Para que la serialización/deserialización funcione correctamente, debes crear una clase que implante las interfaces siguientes:
Crea una sencilla aplicación que cree un examen de “Acceso a Datos” para el 12 de noviembre del 2023 a las 9:45 horas, con 5 estudiantes con nombres de poetas femeninas del siglo XX.
Guarda el examen en una archivo JSON llamado accesoADatos.json, de manera “vistosa” y con formato de fecha anterior mediante el api de Gson y muestre el contenido del archivo por pantalla, utilizando Files de Java NIO.2 y recupere el archivo para guardarlo en un nuevo objeto Java.
Ejercicio 1: Serialización y deserialización básicaSerializar y deserializar una clase sencilla con atributos básicos.
Crea una clase Persona con atributos nombre y edad. Implementa un JsonSerializer y un JsonDeserializer para esta clase, personalizando los nombres de los atributos en el JSON resultante, de modo que aparezcan como name y age en formato JSON.
Ejercicio 2: Serialización y deserialización de objetos anidadosManejar una clase que contiene otro objeto como atributo.
Crea una clase Direccion con atributos calle y ciudad. Añade un atributo de tipo Direccion. Implementa los serializadores y deserializadores necesarios para manejar esta estructura de modo que la dirección tenga el nombre address y aparezca como una cadena de texto con el formato calle, ciudad.
Ejercicio 3: Serialización y deserialización de listasManejar una clase que contiene una lista de objetos.
Añade a la clase Persona una lista de objetos Persona llamados amigos. Implementa los serializadores y deserializadores para manejar la lista de amigos en el JSON. Haz que la lista de amigos la represente como un array de objetos JSON.
Ejercicio 4: Serialización y deserialización de números personalizadosPersonalizar la serialización y deserialización de números.
Crea una clase Producto con atributos nombre y precio. Implementa un JsonSerializer y un JsonDeserializer que formateen el precio como una cadena con dos decimales en el JSON.
Ejercicio 5: Serialización y deserialización de arraysManejar una clase que contiene un array de objetos.
Añade a la clase persona un atributo hobbies. Implementa los serializadores y deserializadores para manejar el array de hobbies en el JSON para que aparezca como una lista de cadenas de texto separadas por guion.
4. Adaptadores de tipo: clase TypeAdapter
El API de Gson incorpora una clase, para declarar adaptaciones de tipos de datos personalizadas, la clase abstracta TypeAdapter.
Dicha clase tiene dos métodos abstractos “read” y “write”.
Definiendo la forma JSON de un tipo
Por defecto, Gson convierte las clases de la aplicación a JSON utilizando sus adaptadores de tipo integrados. Si la conversión JSON predeterminada de Gson no es adecuada para un tipo, debe extenderse esta clase para personalizar la conversión.
Por ejemplo, un adaptador de tipo para un punto (X, Y):
// Adaptador de la clase PointpublicclassPointAdapterextends TypeAdapter<Point> {
// Implantación del método read:public Point read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) { // si el token es null, lo lee y sale. reader.nextNull();
returnnull;
}
String xy = reader.nextString(); // lee la cadena y la consume. String[] coords = xy.split(",");
int x = Integer.parseInt(coords[0]);
int y = Integer.parseInt(coords[1]);
returnnew Point(x, y);
}
// Implantación del método write para escribir el Objeto Java.publicvoidwrite(JsonWriter writer, Point punto) throws IOException {
if (punto ==null) {
writer.nullValue(); // codifica nullreturn;
}
String xy = punto.getX() +","+ punto.getY();
writer.value(xy); // Codifica la cadena (devuelve el JsonWriter que podemos concatenar) }
}
Con este adaptador de tipo registrado, Gson convertirá los puntos a JSON como cadenas como “5,8” en lugar de objetos como {“x”:5,“y”:8}. En este caso, el adaptador de tipo vincula una clase Java a un valor JSON compacto.
El método read() debe leer exactamente un valor y write() debe escribir exactamente un valor.
Para tipos primitivos, esto significa que los readers deben hacer exactamente una llamada a nextBoolean(), nextDouble(), nextInt(), nextLong(), nextString() o nextNull(). Estos métodos devuelven el valor boolean, double, int, long, String o null del siguiente token, consumiéndolo.
Los writers deben hacer exactamente una llamada a value() o nullValue(). “value” codifica el valor y lo escribe directamente.
Para arrays, los adaptadores de tipo deben comenzar con una llamada a beginArray(), convertir todos los elementos y finalizar con una llamada a endArray().
Para objetos, deben comenzar con beginObject(), convertir el objeto y finalizar con endObject(). No convertir un valor o convertir demasiados valores puede hacer que la aplicación se bloquee.
Los adaptadores de tipo deben estar preparados para leer null desde el flujo y escribirlo en el flujo. Alternativamente, deben utilizar el método nullSafe() al registrar el adaptador de tipo con Gson. Si la instancia de Gson ha sido configurada con GsonBuilder.serializeNulls(), estos nulos se escribirán en el documento final. De lo contrario, el valor (y el nombre correspondiente al escribir en un objeto JSON) se omitirá automáticamente. En ambos casos, el adaptador de tipo debe manejar null.
Los adaptadores de tipo deben ser sin estado y seguros para subprocesos; de lo contrario, las garantías de seguridad para subprocesos de Gson podrían no aplicarse.
Para usar un adaptador de tipo personalizado con Gson, debes registrarlo con un GsonBuilder:
GsonBuilder builder =new GsonBuilder();
builder.registerTypeAdapter(Point.class, new PointAdapter());
// Si PointAdapter no comprobó los nulos en sus métodos de lectura/escritura, debes usar en su lugar// builder.registerTypeAdapter(Point.class, new PointAdapter().nullSafe());...
Gson gson = builder.create();
Ejercicio con TypeAdapter
Modifica la clase Examen que contiene los siguientes atributos:
materia: de tipo String.
fecha: LocalDateTime, ahora LocalDateTime.
participantes: de tipo List de String con los nombres de los estudiantes.
Para que la fecha la guarde en formato LocalDateTime, no Date.
Para que la serialización/deserialización funcione correctamente, debes crear una clase que herede la clase TypeAdapter:
Crea una sencilla aplicación que cree un examen de “Acceso a Datos” para el 12 de noviembre del 2023 a las 9:45 horas, con 5 estudiantes con nombres de poetas femeninas del siglo XX.
Guarda el examen en una archivo JSON llamado accesoADatos.json (de manera “vistosa” y con formato de fecha anterior mediante el api de Gson y muestre el contenido del archivo por pantalla, utilizando Files de Java NIO.2 y recupere el archivo para guardarlo en un nuevo objeto Java.
Ejercicio 1: Serialización y deserialización básicaSerializar y deserializar una clase sencilla con atributos básicos.
Crea una clase Persona con atributos nombre y edad. Implementa un TypeAdapter para esta clase, personalizando los nombres de los atributos en el JSON resultante, de modo que aparezcan como name y age en formato JSON.
Ejercicio 2: Serialización y deserialización de objetos anidados
Manejar una clase que contiene otro objeto como atributo.
Crea una clase Direccion con atributos calle y ciudad. Añade un atributo de tipo Direccion. Implementa los adaptadores de tipo necesarios para manejar esta estructura de modo que la dirección tenga el nombre address y aparezca como una cadena de texto con el formato calle, ciudad.
Ejercicio 3: Serialización y deserialización de listas
Manejar una clase que contiene una lista de objetos.
Añade a la clase Persona una lista de objetos Persona llamados amigos. Implementa los adaptadores de tipo para manejar la lista de amigos en el JSON. Haz que la lista de amigos la represente como un array de objetos JSON.
Ejercicio 4: Serialización y deserialización de números personalizados
Personalizar la serialización y deserialización de números.
Crea una clase Producto con atributos nombre y precio. Implementa un TypeAdapter que formatee el precio como una cadena con dos decimales en el JSON.
Ejercicio 5: Serialización y deserialización de arrays
Manejar una clase que contiene un array de objetos.
Añade a la clase persona un atributo hobbies. Implementa los adaptadores de tipo para manejar el array de hobbies en el JSON para que aparezca como una lista de cadenas de texto separadas por guion.
Un JsonReader permite leer una cadena JSON o un archivo como una secuencia de tokens JSON, JsonToken.
Iterar token por token en JSON también se conoce como streaming o flujo a través de los tokens JSON. Así, a veces se hace referencia al JsonReader de GSON como un analizador JSON en streaming.
Un flujo incluye:
Elementos literales: cadenas, números, booleanos y nulos.
Delimitadores de inicio y fin de objetos y arrays ({, }, [, ]).
Los tokens de JSON se recorren en profundidad, en el mismo orden en que aparecen en el documento JSON.
Los Objetos JSON, los pares nombre/valor se representan en un único Token.
Los analizadores en streaming suelen implementarse en dos versiones:
Analizadores de extracción (pull parser): analizador en el que el código que lo utiliza extrae los tokens del analizador cuando está listo para gestionar el siguiente token.
Analizadores de empuje (push parser): un push parser analiza los tokens JSON y los envía a un gestor de eventos.
JsonReader de GSON es un pull parser.
1.1 Creación de un JsonReader
Se puede crear un JsonReader de GSON por medio de su constructor (único).
El constructor del JsonReaderrecoge un Reader Java como argumento:
En el ejemplo anterior se lee de un flujo de cadena de tipo StringReader, pasándole el objeto de tipo StringReaderal constructor del JsonReader.
El StringReader es un flujo de tipo Reader que convierte una cadena Java en una secuencia de caracteres (es decir, un Reader).
2. Iteración de los Tokens JSON JsonToken de un JsonReader
Una vez creada una instancia de JsonReader, se puede iterar a través de los tokens JSON que lee del Reader pasado al constructor del JsonReader.
La clase JsonToken tiene constantes de enumeración para identificar el tipo de token:
Constante enumeración
Descripción
BEGIN_ARRAY
Apertura de un array JSON.
BEGIN_OBJECT
Apertura de un objeto JSON.
BOOLEAN
Valor JSON true o false.
END_ARRAY
Cierre de un array JSON.
END_DOCUMENT
Final del flujo JSON.
END_OBJECT
Cierre de un objeto JSON.
NAME
Nombre de una propiedad JSON.
NULL
Valor JSON nulo.
NUMBER
Número JSON representado por un double, long o int en Java.
STRING
String JSON.
Para acceder a los tokens del JsonReader, se puede utilizar un bucle similar al siguiente:
while (jsonReader.hasNext()) {
}
El método hasNext() del JsonReader devuelve true si el tiene más tokens.
String json ="{\"nome\" : \"Alejandra Pizarnik\", \"idade\" : 36}";
JsonReader jsonReader =new JsonReader(new StringReader(json));
try {
while (jsonReader.hasNext()) {
JsonToken siguienteToken = jsonReader.peek(); // devuelve el siguiente, sin consumirlo. System.out.println(siguienteToken);
if (JsonToken.BEGIN_OBJECT== siguienteToken) {
// Si es un objeto, consumimos las llaves { jsonReader.beginObject();
} elseif (JsonToken.NAME== siguienteToken) {
// Si es un nombre de atributo, lo imprimimos. String nomeAtributo = jsonReader.nextName();
System.out.println(nomeAtributo);
} elseif (JsonToken.STRING== siguienteToken) {
// si es una cadena, recuperamos String y la imprimimos String valorString = jsonReader.nextString();
System.out.println(valorString);
} elseif (JsonToken.NUMBER== siguienteToken) {
// Si es un número, OJO con los tipos...long valorNumero = jsonReader.nextLong();
System.out.println(valorNumero);
}
}
} catch (IOException e) {
System.err.println(e.getMessage());
}
También podría haberse hecho con un switch:
String json ="{\"nome\" : \"Alejandra Pizarnik\", \"idade\" : 36}";
JsonReader jsonReader =new JsonReader(new StringReader(json));
while (jsonReader.hasNext()) {
JsonToken siguienteToken = jsonReader.peek(); // devuelve el siguiente, sin consumirlo. System.out.println(siguienteToken);
if (null!= siguienteToken) {
switch (siguienteToken) {
case BEGIN_OBJECT ->// Si es un objeto, consumimos las llaves { jsonReader.beginObject();
case NAME -> {
// Si es un nombre de atributo, lo imprimimos. String nomeAtributo = jsonReader.nextName();
System.out.println(nomeAtributo);
}
case STRING -> {
// si es una cadena, recuperamos String y la imprimimos String valorString = jsonReader.nextString();
System.out.println(valorString);
}
case NUMBER -> {
// Si es un número, OJO con los tipos...long valorNumero = jsonReader.nextLong();
System.out.println(valorNumero);
}
default-> {
}
}
}
}
El método peek() del JsonReader devuelve el siguiente token JSON, pero sin moverse sobre él (sin devolver el siguiente). Sucesivas llamadas a peek() devolverán el mismo token JSON.
El JsonToken devuelto por peek() se puede comparar con constantes en la clase JsonToken para averiguar qué tipo de token es. Puedes ver cómo se hace esto en el bucle del código anterior.
Dentro de cada declaración if, se llama a un método del JsonReader, next _TipoDato_ () , lee del JsonReader el token actual y avanza al siguiente.
Todos los métodos beginObject(), nextString() y nextLong()devuelven el valor del token actual y mueven el puntero interno al siguiente Token.
3. “Parser” personalizado de JSON con JsonReader
Para analizar (“parsear”) un flujo JSON por medio de un JsonReader mediante un parser descendente recursivo:
a) Creamos un método inicial que cree un JsonReader y llame a un método de lectura de un array de objetos JSON. Finalmente, cierra el JsonReader:
- El método principal de entrada crea un JsonReader a partir de un InputStream (que convertiremos en un Reader) o un Reader.
- Llama a un método de lectura de los tokens JSON de un array o de objetos JSON.
- Finalmente, cierra el JsonReader.
import com.google.gson.stream.JsonReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
publicclassPoemaJsonReader {
// Método principal de entradapublic List<Poema>readJsonStream(InputStream in) throws IOException {
// JsonReader necesita un Reader, por lo que convertimos el InputStream en un InputStreamReader// Además, implanta la interfaz Closeable, por lo que podemos cerrarlo en un bloque try-with-resourcestry(JsonReader reader =new JsonReader(new InputStreamReader(in, "UTF-8"))) {
return readArrayPoemas(reader);
}
}
}
b) Creamos métodos de gestión/control para cada estructura del objeto JSON. Se necesita un método para cada tipo de objeto y para cada tipo de array:
Dentro de los métodos de gestión de arrays, primero llamamos a beginArray() para consumir el corchete de apertura del array. Luego, se crea un bucle while que acumula valores, terminando cuando hasNext() sea false. Finalmente, se lee el corchete de cierre del array llamando a endArray().
Dentro de los métodos de gestión de objetos, primero se invoca a beginObject() para consumir la llave de apertura del objeto. Luego, crea un bucle while que asigna valores a variables locales según su nombre. Este bucle debe terminar cuando hasNext() sea false. Finalmente, se lee la llave de cierre del objeto llamando a endObject().
import com.google.gson.stream.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
publicclassPoemaJsonReader {
// Método principal de entrada// .../**
* Ejemplo que lee un array de objetos JSON
* Devuelve la lista de poemas del JSON
* */public List<Poema>readArrayPoemas(JsonReader reader) throws IOException {
// Guardar la lista de poemas del JSON List<Poema> poemas =new ArrayList<>();
reader.beginArray(); // Leemos el [while (reader.hasNext()) { // para cada elemento de array de poemas poemas.add(readPoema(reader));
}
reader.endArray(); // Leemos el ]return poemas;
}
public Poema readPoema(JsonReader reader) throws IOException {
// Código de lectura de un poema }
}
Cuando se encuentra un objeto o array anidado, delega al método de control correspondiente.
Cuando se encuentra un nombre desconocido, los analizadores estrictos deberían fallar con una excepción. Los analizadores permisivos deben llamar a skipValue() para omitir de forma recursiva los tokens anidados del valor, que de lo contrario podrían entrar en conflicto.
Si un valor puede ser nulo, debes verificar primero utilizando peek(). Los literales nulos se pueden consumir utilizando nextNull() o skipValue().
Teniendo en cuenta que las varias propiedades identifican un identificador del icono con un número y que el icono está asociado al número por medio de la URL:
PrediccionDia: dataPrediccion, nivelAviso (int), tMax, tMin, uvMaz, y una List de objetos VariableFranxa con los posibles valores de VariableMeteoroloxia:
Recuerda que con new URI(URL).toURL().openConnection().getInputStream() puedes obtener el flujo de datos de entrada. Debes convertirlo en Reader.
c. Crea un adaptador para ese Json completo, llamado PrediccionDeselializer implements JsonDeserializer<Prediccion> que permita obtener un objeto Java de tipo Prediccion con los datos del Concello, únicamente:
Santiago de Compostela [15078]
Haz pruebas con el adaptador para que muestre la información del Concello. Recuerda registrar el adaptador en el objeto GsonBuilder y hacer pruebas con varios concellos.
d. Haz un adaptador para PrediccionDia, public class PrediccionDiaDeserializer implements JsonDeserializer<PrediccionDia>, para que permita adaptar el siguiente JSON, pero solo los atributos de primer nivel, no las variables de franxa (ten en cuenta que los atributos pueden cambiar de nombre y que hay elementos nulos):
e. Haz que el adaptador PrediccionAdapter llame al adaptador PrediccionDiaAdapter para que adapte los datos de PrediccionDia.
Ayuda: para que el adaptador de Predicción llame al de PredicciónDia, debes invocar al método deserialize del contexto pasándole el objeto Json y el tipo de objeto a deserializar:
<T> T deserialize (JsonElement json, Type typeOfT) throws JsonParseException
// e es el JsonElement que se pasa al adaptador, en nuestro caso el Json de PrediccionDia que hemos leído en PrediccionAdapter.pr.addPredDiaConcello(contexto.deserialize(e, PrediccionDia.class));
f. Amplía PrediccionDiaDeserializer para que recupere los datos de las variables de franxa.
f.1. Crea un método privado en PrediccionDiaDeserializer llamado getVariableFranxa que recoja el tipo de variable meteorológica y el objeto Json con los valores y devuelva un objeto de tipo VariableFranxa con la siguiente firma:
En el que el objeto varFranxaJsonObject es el que tiene el formato:
{
"manha": 18,
"noite": 16,
"tarde": 20},
f.2. Recupera los valores de VariableFranxa del objeto Json de PrediccionDia. Existen varios modos de hacerlo:
Recorrer todos los atributos del objeto JSON y, si son de tipo objeto, llamar a un método que los lea o hacerlo en el propio bucle:
public PrediccionDia deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// ...for (Map.Entry<String, JsonElement> entrada : jsonObject.entrySet()) {
if (entrada.getValue().isJsonObject()) { // ¡ES UNA VARIABLE DE FRANXA! entrada.getKey(); // Es el nombre de la variable de franxa, de tipo String entrada.getValue().getAsJsonObject(); // Es el objeto JSON de la variable de franxa }
}
// ... }
Recorrer todos los posibles valores de VariableMeteo y obtener el objeto asociado a ese valor:
public PrediccionDia deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// ...for (VariableMeteo variable : VariableMeteo.values()) {
if (jsonObject.has(variable.getNome())) { // tiene esa variable de franxa getVariableFranxa(variable, jsonObject.get(variable.getNome()).getAsJsonObject());
}
}
// ... }
f.3. Modifica el método toString() de VariableMeteo para que imprima según la siguiente regla:
g.Recuperación de la lista de Concellos de MeteoGalicia. El json con la lista de concellos está compartido en “común”: concellos.json, y tiene el siguiente formato:
Haz un adaptador para recuperar la lista de concellos. Crea una clase ConcelloAdapter que permita recuperar la lista de concellos de MeteoGalicia. Este adaptador debe ser de tipo TypeAdapter<List<Concello>> y debe devolver una lista de objetos de tipo Concello.
Haz un programa que muestre la lista de concellos.
h. Dado el archivo Json con los concellos clasificados por Provincia, crea una clase Provincia, que tenga un atributo nombre y una lista de concellos. Crea un adaptador para recuperar la lista de provincias y los Concellos asociados.
import java.util.List;
publicinterfaceDao<T, K> {
T getById(K id);
T getByName(String name);
List<T>getAll();
}
a) Crea una clase ConcelloDao que implemente la interface Dao<Concello, Integer>.:
Debe tener dos atributos estáticos:
CONCELLOS_JSON con el nombre del archivo JSON con la lista de concellos (concellos.json).
TIPO a emplear en las listas de concellos: new TypeToken<List<Concello>>() {}.getType().
La clase ConcelloDao debe tener un atributo concellos de List<Concello> con la lista de concellos.
Declara dos constructores:
No recoja nada y obtenga la lista de concellos del archivo JSON por defecto, CONCELLOS_JSON.
Un constructor que recoja un InputStream/Reader al archivo JSON y cargue la lista de concellos de ese recurso.
Los métodos implantados de la interfaz DAO son:
getById(Integer idConcello) y getByName(String nomeConcello).
getAll().
a) Crea una clase ProvinciaDao que implemente la interfaz Dao<Provincia, Integer>.:
Debe tener dos atributos estáticos:
PROVINCIAS_FILE con el nombre del archivo JSON con la lista de provincias (provincias.json).
TIPO a emplear en las listas de provincias: new TypeToken<List<Provincia>>() {}.getType().
La clase ProvinciaDao debe tener un atributo provincias de List<Provincia> con la lista de provincias.
Declara dos constructores:
Uno que no recoja nada y obtenga la lista de provincias del archivo JSON por defecto, PROVINCIAS_FILE.
Un constructor que recoja un InputStream/Reader al archivo JSON y cargue la lista de provincias de ese recurso.
Los métodos implantados de la interfaz DAO son:
getById(Integer idProvincia) y getByName(String nomeProvincia).
getAll().
c) Crea una clase PrediccionDao que implemente la interfaz Dao<Prediccion, Integer>:
Debe tener dos atributos estáticos:
BASE_URL con la ruta base para la predicción de un concello: "https://servizos.meteogalicia.gal/mgrss/predicion/jsonPredConcellos.action?idConc="
Atributos:
gson de tipo Gson para la deserialización de los objetos JSON.
concelloDao de tipo ConcelloDao para la recuperación de los id de los concellos a partir del nombre o la lista de concellos.
Constructores:
Uno que no recoja nada y cree un ConcelloDao por defecto, así como un Gson con el adaptador de Prediccion.
Un constructor que recoja un ConcelloDao y cree Gson para la deserialización de los objetos JSON con el adaptador de Prediccion.
Un constructor que recoja un ConcelloDao y un Gson para la deserialización de los objetos JSON.
Implantación de los métodos de la interfaz DAO:
getById(Integer idConcello) que recupera la predicción de un concello a partir de su id.
getByName(String nomeConcello) que recupera la predicción de un concello a partir de su nombre.
getAll() que recupera la predicción de todos los concellos.
j. Mejora el programa AppPredicción para que pueda mostrar la predicción de cualquier concello. Para ello, deberás solicitar al usuario el id, comprobar que existe y mostrar la predicción de ese concello.
Haz un menú que permita al usuario buscar el nombre de un concello de modo que contenda la cadena escrita, mostrando los ids de esos concellos. Después, debe permitir al usuario introducir el id del concello y mostrar la predicción.
Muchas veces los nombres de los atributos de los objetos JSON no coinciden con los de la clase Java, bien porque es una fuente externa, porque está compartido por otras aplicaciones o porque la clase ya está compilada y no tenemos acceso al código fuente.
Existen varias formas de mapear los atributos JSON a los atributos de una clase Java:
Por medio adaptadores personalizados (TypeAdapter) con estrategias de serialización/deserialización (JsonSerializer<T>, JsonDeserializer<T>) en GSON.
Reglas de nombrado
También puede establecer una política de nomenclatura diferente utilizando la clase GsonBuilder: GsonBuilder.setFieldNamingPolicy(com.google.gson.FieldNamingPolicy) (IDENTITY, UPPER_CAMEL_CASE,…) para el formato de los atributos JSON, asignando un valor de la enumeración:
IDENTITY: con esta política de nomenclatura, el nombre del atributo no cambia.
LOWER_CASE_WITH_DASHES: modifica el nombre del atributo Java del formato CamelCase a un nombre de atributo en minúsculas donde cada palabra está separada por un guión (-).
LOWER_CASE_WITH_UNDERSCORES : modifica el nombre del atributo Java del formato en CamelCase a un nombre de atributo en minúsculas donde cada palabra está separada por un guión bajo (_)
UPPER_CAMEL_CASE : asegura que la primera “letra” del nombre del atributo Java esté en mayúscula cuando se serialice en su formato JSON.
UPPER_CAMEL_CASE_WITH_SPACES : garantiza de que la primera “letra” del nombre del atributo Java esté en mayúscula cuando se serialice en su formato JSON y que las palabras estén separadas por un espacio.
Pregunta: ¿Por qué no está CamelCase, únicamente? Porque espero que hayas empleado la nomenclatura estándar. ¿Verdad?
Esta anotación indica que este miembro debe ser serializado a JSON con el valor proporcionado como su nombre de atributo.
El uso de esta anotación anulará cualquier FieldNamingPolicy, incluida la política de nomenclatura de campo predeterminada, que pueda haberse establecido en la instancia Gson.
publicclassPoeta {
@SerializedName("nomePoeta") String nome;
@SerializedName(value="idadePoeta", alternate={"idadePoeta2", "idadePoeta3"}) int idade;
String c; // Otro atributopublicPoeta(String nome, String idade, String c) {
this.nome= nome;
this.idade= idade;
this.c= c;
}
}
La salida generada al serializar una instancia de la clase Poeta:
Poeta poeta =new Poeta("Alejandra Pizarnik", 36, "La vida es dura");
Gson gson =new Gson();
String json = gson.toJson(poeta);
System.out.println(json);
Salida:
{"nomePoeta":"Alejandra Pizarnik","idadePoeta":36,"c":"La vida es dura"}
NOTA: el valor que se especifique en esta anotación debe ser un nombre de campo JSON válido.
Al deserializar, todos los valores especificados en la anotación se deserializarán en el atributo. Por ejemplo el mapeado de la edad tiene múltiples nombres:
Poeta poeta = gson.fromJson("{'idadePoeta':36}", Poeta.class);
Assert.assertEquals(36, poeta.idade);
poeta = gson.fromJson("{'idadePoeta2':25}", Poeta.class);
Assert.assertEquals(25, poeta.idade);
poeta = gson.fromJson("{'idadePoeta3':35}", Poeta.class);
Assert.assertEquals(35, poeta.idade);
import org.junit.Assert; para aserciciones.
Ten en cuenta que Poeta.idade se deserializa ahora desde cualquiera de los campos idadePoeta, idadePoeta2 o idadePoeta3.
3. Estrategias de nombrado: FieldNamingStrategy
La interface FieldNamingStrategy es otra opción que tenemos en Gson para personalizar cómo se deben convertir los nombres de los atributos. Nos permite definir una estrategia propia para convertir los nombres de los campos en JSON.
Se trata de una interface funcional con un único método, por lo que es muy útil el uso de expresiones lambda:
public String translateName(Field f);
Es importante que invoquemos al método setFieldNamingStrategy de GsonBuilder para que tenga efecto.
Por ejemplo:
import com.google.gson.*;
publicclassAppEjemploEstrategia {
publicstaticvoidmain(String[] args) {
Gson gson =new GsonBuilder()
.setFieldNamingStrategy(new EstrategiaNombres()) // Mejor con lambda .create();
Poeta poeta =new Poeta("Alejandra Pizarnik", 25);
// Serialización String jsonPoeta = gson.toJson(poeta);
System.out.println("Serializado: "+ jsonPoeta);
// Deserialización Poeta poetaDeserializado = gson.fromJson(jsonPoeta, Poeta.class);
System.out.println("Deserializado: "+ poetaDeserializado);
}
staticclassPoeta {
private String nome;
privateint idade;
publicPoeta(String nome, int idade) {
this.nome= nome;
this.idade= idade;
}
@Overridepublic String toString() {
return"Poeta {"+"nome='"+ nome +'\''+", idade="+ idade +'}';
}
}
staticclassEstrategiaNombresimplements FieldNamingStrategy {
@Overridepublic String translateName(Field f) {
// Personaliza cómo se deben convertir los nombres de los atributosif (f.getName().equals("nome")) {
return"nombre";
} else {
return f.getName();
}
}
}
}
Clase java.lang.reflect.Field
La clase Field, es del API de Java, java.lang.reflect.Field, y proporciona información y acceso dinámico a un único campo de una clase o interfaz.
Además de getName(), tiene otros métodos get como; getType(), para obtener la clase tipo; getChar(), getDouble(); etc. También tiene métodos set para todos los tipos de datos, entre otros.
La ventaja de usar FieldNamingStrategy es que es más general y se aplica a todos los atributos en cualquier clase, mientras que los adaptadores personalizados son específicos para una clase en particular.
4. Uso de adaptadores personalizados (TypeAdapter)
Hemos visto que la clase GsonBuilder dispone de un método:
public GsonBuilder registerTypeAdapter (Type type, Object typeAdapter)
Empleando este método nos permite otra forma flexible de mapear los atributos JSON a los atributos de una clase Java sin depender únicamente de la anotación @SerializedName, mediante el uso de adaptadores personalizados y estrategias de serialización/deserialización en GSON.
Se peuden crear adaptadores personalizados implantando las interfaces JsonSerializer y JsonDeserializer para proporcionar una lógica personalizada de cómo se deben serializar y deserializar los campos.
En este ejemplo, el adaptador personalizado AdaptadorNombres controla cómo se deben serializar y deserializar los campos de Poeta.
La ventaja con respecto al anterior, es que podemos, fácilmente, adaptar de manera distinta cada clase Java.
Ejercicio. Búsqueda de códigos postales
Existen muchas API libres o de código abierto en Internet. Una de las más curiosas es la que devuelve la localización a la que pertenece un código postal:
1. Crea las clases del modelo Java necesarias para la conversión a archivos Json: Lugar y CodigoPostal.
Emplea estándares de nombres y conversión de tipos de datos (los números no deben representarse como cadenas de texto). Usa nombres significativos en gallego/castellano, como consideres. Sobrescribe los métodos toString, equals y hashCode. Ten en cuenta que el código postal puede hacer referencia a varios lugares y que un lugar solo puede tener un único código postal:
publicclassLugar {
private String nome;
privatedouble longitud;
privatedouble latitud;
private String estado;
private String abreviaturaEstado;
// .../**
* Método que devuelve un String con los datos del lugar en formato HTML con colores.
* @return String con los datos del lugar en formato HTML con colores.
*/public String toHTML() {
return"<h1>"+ nome +"</h1>"+"Longitud: "+ longitud +"<br/>"+"Latitud: "+ latitud +"<br/>"+"Comunidad: "+ estado +"<br/>"+"Abreviatura Comunidad: "+ abreviaturaEstado +"<br/>";
}
/**
* Método que recoge un boolean si quiero devolver el lugar en formato fila de una tabla HTML.
* Devuelve un String con los datos del lugar en formato HTML con colores.
* Si está en una fila de una tabla HTML, el fondo de la fila es de color gris.
* @param fila boolean que indica si quiero devolver el lugar en formato fila de una tabla HTML.
*/public String toHTML(boolean fila) {
return (fila) ?"<tr style=\"background-color: #cccccc\">"+"<td>"+ nome +"</td>"+"<td>"+ longitud +"</td>"+"<td>"+ latitud +"</td>"+"<td>"+ estado +"</td>"+"<td>"+ abreviaturaEstado +"</td>"+"</tr>" : "<h1>"+ nome +"</h1>"+"Longitud: "+ longitud +"<br/>"+"Latitud: "+ latitud +"<br/>"+"Comunidad: "+ estado +"<br/>"+"Abreviatura Comunidad: "+ abreviaturaEstado +"<br/>";
}
/**
* Método que devuelve un String con los datos del lugar en formato texto.
* @return String con los datos del lugar en formato texto.
*/@Overridepublic String toString() {
return" Lugar: "+ nome + System.lineSeparator()
+" Longitud: "+ longitud + System.lineSeparator()
+" Latitud: "+ latitud + System.lineSeparator()
+" Comunidad: "+ estado + System.lineSeparator()
+" Abreviatura Comunidad: "+ abreviaturaEstado + System.lineSeparator();
}
//...}
publicclassCodigoPostal {
private String codigoPostal;
private String pais;
private String abreviaturaPais;
private List<Lugar> lugares;
//.../**
* Devuelve la lista de lugares como HTML, empleando un forEach para concatenar los lugares.
* El método forEach recibe un Consumer, que es una interfaz funcional que tiene un método
* abstracto accept() que recibe un objeto de tipo T y no devuelve nada (void).
*
* @return cadena de texto con los lugares en formato HTML
*/public String getLugaresAsHTML() {
StringBuilder sb =new StringBuilder("<html><body>");
lugares.forEach(lugar -> {
sb.append(lugar.toHTML()).append("<br>");
});
sb.append("</body></html>");
return sb.toString();
}
/**
* Método que devuelve la lista de lugares como HTML, empleando un forEach para concatenar los lugares.
* El método forEach recibe un Consumer, que es una interfaz funcional que tiene un método
* abstracto accept() que recibe un objeto de tipo T y no devuelve nada (void).
*
* @param asTable boolean que indica si quiero devolver los lugares en formato fila de una tabla HTML.
* @return cadena de texto con los lugares en formato HTML
*/public String getLugaresAsHTML(boolean asTable) {
StringBuilder sb =new StringBuilder("<html><body>");
if (asTable) {
sb.append("<table border=\"1\">");
sb.append("<tr style=\"background-color: #cccccc\">");
sb.append("<th>Lugar</th>");
sb.append("<th>Longitud</th>");
sb.append("<th>Latitud</th>");
sb.append("<th>Comunidad</th>");
sb.append("<th>Abreviatura Comunidad</th>");
sb.append("</tr>");
lugares.forEach(lugar -> {
sb.append(lugar.toHTML(true));
});
sb.append("</table>");
} else {
lugares.forEach(lugar -> {
sb.append(lugar.toHTML()).append("<br>");
});
}
sb.append("</body></html>");
return sb.toString();
}
@Overridepublic String toString() {
var sb =new StringBuilder("Código Postal: '"+ codigoPostal + System.lineSeparator()
+"Pais: '"+ pais + System.lineSeparator()
+"AbreviaturaPais: "+ abreviaturaPais + System.lineSeparator());
lugares.forEach(lugar -> {
sb.append(lugar).append(System.lineSeparator());
});
return sb.toString();
}
}
3. Crea una clase CodigoPostalDAO que implemente la siguiente interfaz:
publicinterfaceICodigoPostalDAO {
/**
* Obtiene un objeto CodigoPostal a partir de un código postal.
* @param codigoPostal Código postal como cadena de texto.
* @return Objeto CodigoPostal o null si no se ha podido obtener.
*/public CodigoPostal getCodigoPostal(String codigoPostal);
/**
* Obtiene un objeto CodigoPostal a partir de un código postal y un país.
* @param codigoPostal Código postal como cadena de texto.
* @param pais País como cadena de texto ("es", "fr", "us", etc.)
* @return Objeto CodigoPostal o null si no se ha podido obtener.
*/public CodigoPostal getCodigoPostal(String codigoPostal, String pais);
}
4. Crea una aplicación que, dado el código postal, muestre la lista de lugares que corresponden con ese código mediante el patrón Model-View-Controller:
Interfaces de la vista y del controlador:
publicinterfaceICodigoPostalController {
/**
* Obtiene la lista de lugaros a partir de un código postal. Si no se encuentra el código postal
* devuelve null.
*
* @param codigoPostal Código postal como cadena de texto
* @param asHTML Devuelve los datos en formato HTML
* @return Lista de lugares como cadena o null si no se ha podido obtener
*/public String getLugares(String codigoPostal, boolean asHTML);
/**
* Obtiene la lista de lugares a partir de un código postal y un país. Si no se encuentra el código postal
* devuelve null.
*
* @param codigoPostal Código postal como cadena de texto
* @param pais País como cadena de texto ("es", "fr", "us", etc.)
* @param asHTML Devuelve los datos en formato HTML
* @return Lista de lugares como cadena o null si no se ha podido obtener
*/public String getLugares(String codigoPostal, String pais, boolean asHTML);
/**
* Asigna la lista de lugares a la vista a partir de un código postal con el pais por defecto.
* Si no se encuentra el código postal devuelve null.
* @param codigoPostal
* @param asHTML
*/publicvoidsetLugares(String codigoPostal, boolean asHTML);
publicvoidsetVista(IVistaCodigoPostal vista);
}
Vista:
publicinterfaceIVistaCodigoPostal {
// Métodos para mostrar los datos al usuario/**
* Muestra un mensaje de error en la vista.
*/voidmostrarError(String mensaje);
/**
* Muestra añade un lugar a la lista de lugares de la vista.
*/publicvoidaddLugar(String lugar);
/**
* Borra la lista de lugares de la vista.
*/publicvoiddeleteLugares();
/**
* Añade un código postal a la lista de códigos postales de la vista.
*/publicvoidsetLugares(String lugares);
/**
* Asigna un controlador a la vista.
* @param controller Controlador a asignar.
*/publicvoidsetController(ICodigoPostalController controller);
/**
* Muestrea la vista
*/publicvoidmostrar();
}
La implementación del controlador debe tener referencias a la vista y al modelo (clase DAO):
publicclassCodigoPostalControllerimplements ICodigoPostalController {
// El controlador debe tener una referencia al modelo y a la vista.private ICodigoPostalDAO codigoPostalDAO;
private IVistaCodigoPostal vistaCodigoPostal;
// Constructor que recoge la referencia al modelopublicCodigoPostalController(ICodigoPostalDAO codigoPostalDAO, IVistaCodigoPostal vistaCodigoPostal) {
this.codigoPostalDAO= codigoPostalDAO;
this.vistaCodigoPostal= vistaCodigoPostal;
}
publicCodigoPostalController(IVistaCodigoPostal vistaCodigoPostal) {
codigoPostalDAO =new CodigoPostalDAO();
this.vistaCodigoPostal= vistaCodigoPostal;
}
publicCodigoPostalController() {
codigoPostalDAO =new CodigoPostalDAO();
}
//...}
5.. Diseña una vista de texto y otra en JAva Swing que funcione con este controlador e implante dicha interfaz:
publicclassVentaCodigoPostalextends JFrame implements IVistaCodigoPostal {
// Controlador de la vistaprivate ICodigoPostalController codigoPostalController;
// Panel principal de la ventana en la que se muestran los datos.private JEditorPane panelDatos; // Por ejemplo// Panel principal de la ventanaprivate JPanel panelPrincipal; // Por ejemplo// ...}
Haz lo mismo, pero de modo que recoja la localidad (de Galicia o España) y muestre los códigos postales de la misma. Inspecciona el JSON para tomar las decisiones de diseño que consideres oportunas.
Primero, serialicemos un array de objetos con Gson:
Pelicula[] arrayPelis = {
new Pelicula(1959, "Los cuatrocientos golpes"),
new Pelicula(1937, "La gran ilusión")};
String jsonPelis =new Gson().toJson(arrayPelis);
// El resultado será igual al siguiente String:String resultado ="[{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\"},{\"ano\":1937,\"titulo\":\"La gran ilusión\"}]";
3. Serializar una Colección de objetos
Una colección de objetos (List,…):
Collection<Pelicula> listaPelis =Lists.newArrayList(new Pelicula(1959, "Los cuatrocientos golpes"),
new Pelicula(1937, "La gran ilusión"));
String jsonPelis =new Gson().toJson(listaPelis);
String resultado ="[{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\"},{\"ano\":1937,\"titulo\":\"La gran ilusión\"}]";
4. Cambio de nombres en Serialización
Podemos cambiar el nombre del campo cuando estamos serializando un objeto (también) con JsonSerializer.
La película, que contiene los campos ano y titulo, los vamos a cambiar en JSON por año y título:
También ten en cuenta que probablemente necesitemos hacer esto en casos donde no podemos cambiar el código fuente de la clase, o si el campo debe ignorarse en casos muy concretos. De lo contrario, podemos ignorar el campo más fácilmente con una anotación directa en la clase de entidad.
6. Serializar un campo si cumple con una condición
Un caso más avanzado podría ser si queremos serializar un campo cuando cumple con una condición concreta y personalizada.
Por ejemplo, si queremos serializar el valor entero si es positivo y omitirlo si es negativo:
Pelicula pelicula =new Pelicula(1996, "Breaking the Waves");
GsonBuilder gsonBuilder =new GsonBuilder();
gsonBuilder.registerTypeAdapter(Pelicula.class,
new SerilizadorIgnoraCampoCondicion());
Gson gson = gsonBuilder.create();
// empleamos: String toJson (Object src, Type typeOfSrc) Type peliculaType =new TypeToken<Pelicula>() {}.getType();
String jsonPelicula = gson.toJson(pelicula, peliculaType);
String resultado ="{\"titulo\":\"Breaking the Waves\"}";
}
publicclassContenedorGenerico<T> {
public T valor;
}
Como ejemplo, deserializemos un JSON para el tipo: ContenedorGenerico<Integer>
// Es importante conocer cómo se obtiene el tipo de datos en éste caso.Type tipoToken =new TypeToken<ContenedorGenerico<Integer>>() { }.getType();
String json ="{\"valor\":8}";
ContenedorGenerico<Integer> enteiro =new Gson().fromJson(json, tipoToken);
// El valor debe coincidir con el Integer 8// assertEquals(enteiro.valor, new Integer(8));
Obtención del tipo de dato
Como regla general, el tipo de dato si es una clase se nombra como MiClase.class. Sin embargo, en algunas situaciones (con genéricos) dicha expresión es incorrecta. En ese caso debe hacerse así:
Crear un TypeToken para la clase concreta y obtener su tipo:
Type tipoToken =new TypeToken<TipoGenerico>() { }.getType();
*Representa un tipo genérico T. Java aún no proporciona una forma de representar tipos genéricos, así que esta clase lo hace. Obliga a los clientes a crear una subclase de esta clase que permite recuperar la información del tipo incluso en tiempo de ejecución.
*
Por ejemplo, para crear un literal de tipo para List<String>, puedes crear una clase anónima vacía:
TypeToken<List<String>> list =new TypeToken<List<String>>() {};
Evita capturar una variable de tipo como argumento de tipo de un TypeToken. Debido al borrado de tipo, el tipo de ejecución de una variable de tipo no está disponible para Gson y, por lo tanto, no puede proporcionar la funcionalidad que uno podría esperar, lo que da una falsa sensación de seguridad en tiempo de compilación y puede llevar a una ClassCastException inesperada en tiempo de ejecución.
Si los argumentos de tipo del tipo parametrizado solo están disponibles en tiempo de ejecución, por ejemplo, cuando deseas crear un List<E> basado en un Class<E> que representa el tipo de elemento, se puede utilizar el método getParameterized(Type, Type...).
4. Deserializar JSON atributos adicionales a un objeto
Deserialicemos un JSON complejo que contiene campos adicionales y desconocidos:
El deserializador personalizado debe analizar los atributos de la cadena JSON y asignarlos al objeto Pelicula:
publicclassCambiaNombresDeserializerimplements JsonDeserializer<Pelicula> {
// El método devuelve la Pelicula@Overridepublic Pelicula deserialize (JsonElement jElement, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
// Recogemos los valores del objeto JSON JsonObject jObject = jElement.getAsJsonObject();
int ano = jObject.get("anoPelicula").getAsInt();
String titulo = jObject.get("tituloPelicula").getAsString();
// creamos la Pelicula y la devolvemosreturnnew Pelicula(ano, titulo);
}
}
6. Deserializar un array JSON a un array de objetos Java
Por ejemplo, deserializamos un array JSON en un array de objetos Pelicula:
7. Deserializar un array JSON a una Collection Java (List,…)
Se puede deserializar directamente un array JSON en un objeto de tipo Collection:
String json ="[{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\"},"+"{\"ano\":1998,\"titulo\":\"Los idiotas\"}]";
// Es el único punto "confluctivo", obtener el tipo de una colección o genérico:Type tipoClaseDestino =new TypeToken<ArrayList<Pelicula>>() { }.getType();
Collection<Pelicula> coleccion =new Gson().fromJson(json, tipoClaseDestino);
//assertThat(coleccion, instanceOf(ArrayList.class));
8. Deserializar un JSON a objectos anidados
Ahora, definamos una clase anidada, PeliculaConDirector:
publicclassPeliculaConDirector {
publicint ano;
public String titulo;
public Director director;
publicclassDirector {
public String nome;
}
}
Y así es como deserializamos una entrada que contiene este objeto anidado:
9. Deserializar JSON con un constructor personalizado
Hemos visto cómo declarar un constructor específico durante las deserializaciones en lugar del constructor predeterminado sin argumentos, utilizando InstanceCreator:
{
"error": false,
"category": "Programming",
"type": "twopart",
"setup": "¿Por qué C consigue todas las chicas y Java no tiene ninguna?",
"delivery": "Porque C no las trata como objetos.",
"flags": {
"nsfw": false,
"religious": false,
"political": false,
"racist": false,
"sexist": false,
"explicit": false },
"safe": true,
"id": 6,
"lang": "es"}
Se pide:
a) Crea las clases Java que consideres adecuada para la aplicación, empleando la nomenclatura estándar y guardando las banderas en una enumeración. Los atributos de las clases no tiene que ajustarse a los del archivo JSON.
b) Crea una clase ChisteDAO que obtenga los chistes del API. Al menos debe tener: getChiste(), que devuelve uno aleatorio; getChisteByLang(String Lang); getChisteByCategory(String category).
c) Haz una aplicación con un menú que pida un tipo de chiste y lo muestre por pantalla. Si lo deseas, haz una aplicación gráfica.
Se desea realizar una aplicación para gestionar Preguntas de Trivial. La aplicación debe permitir la creación de preguntas de dos tipos elección múltiple y verdadero falso.
Emplearemos como base la estructura de datos del api Open Trivia Database que proporciona preguntas en formato JSON.
A modo de ejemplo, podéis consultar el siguiente JSON:
{
"response_code": 0,
"results": [
{
"type": "multiple",
"difficulty": "medium",
"category": "Science: Computers",
"question": "What is the correct term for the metal object in between the CPU and the CPU fan within a computer system?",
"correct_answer": "Heat Sink",
"incorrect_answers": [
"CPU Vent",
"Temperature Decipator",
"Heat Vent" ]
}
]
}
O una de verdadero o falso:
{
"response_code": 0,
"results": [
{
"type": "boolean",
"difficulty": "medium",
"category": "Science: Computers",
"question": "The programming language 'Python' is based off a modified version of 'JavaScript'.",
"correct_answer": "False",
"incorrect_answers": [
"True" ]
}
]
}
Modelo de datos
De momento implanta las opciones como una lista de cadenas en la clase Pregunta.
classDiagram
direction BT
class Pregunta
class PreguntaMultiple
class PreguntaVerdaderoFalso
PreguntaMultiple --> Pregunta
PreguntaMultiple "1" *--> "opciones *" Opcion
PreguntaVerdaderoFalso --> Pregunta
1. Clase final Opcion
Clase final que representa cada una de las opciones de una pregunta tipo test.
Atributos
enunciado: con el enunciado de la opción de la pregunta tipo test.
correcta: que indica si es una opción correcta o no.
Constructor
Un constructor sin parámetros.
Un constructor que recoge el enunciado, marcándola como incorrecta.
Un constructor que recoge el enunciado y si es correcta o no.
Métodos (funciones miembro):
Sobrescribe toString para que devuelva el enunciado. Si la opción es correcta devuelve el enunciado con un [*] al final de la cadena. Verifica nulos.
3. Clase final Categoria
Clase Categoria con un único atributo llamado nombre que recoja el nombre de la categoría de la pregunta. La clase debe tener:
Atributos
Una contante DEFAULT_CATEGORY con el valor “General” que se empleará como categoría por defecto.
Un atributo final nombre para el nombre de la categoría.
Constructores
Un constructor que recoge el nombre de la categoría.
Un constructo por defecto que inicializa el nombre a “Sin categoría”.
Métodos
Sobrescribe los métodos equals y hashCode para que dos categorías sean iguales si tienen el mismo nombre.
Sobrescribe el método toString para que devuelva el nombre de la categoría.
2. Clase Pregunta
Clase Pregunta implementa la interfaz Comparable<Pregunta> y Serializable. Las preguntas tienen:
Identificador de la pregunta, de tipo Long (clase contenedora): idPregunta. Es mutable. En primera instancia no lo vamos a emplear pero es necesario para futuras ampliaciones.
TipoPregunta: enumeración con los valores BOOLEAN y MULTIPLE. La enumeración debe tener:
Un atributo tipoPregunta que guarde el tipo de pregunta en forma de cadena, que tendrá los valores Verdadero/Falso y Multiple.
un método getTipoPregunta() que devuelva el tipo de pregunta.
Un método estático que recoja una cadena y devuelva el tipo de pregunta de tipo enumerado: public static TipoPregunta getTipoPregunta(String tipoPregunta)
dificultad, de tipo Dificultad: enumeración con los valores EASY, MEDIUM, HARD. La enumeración debe tener:
Un atributo dificultad que guarde la dificultad de la pregunta en forma de cadena.
un método getDificultad que devuelva la dificultad de la pregunta.
Un método estático getDificultad que recoja una cadena y devuelva la dificultad de la pregunta de tipo enumerado.
categoria: de tipo Categoria
pregunta: enunciado de la pregunta.
Constructor
Dos constructores:
Un constructor por defecto.
Un constructor que recoge el enunciado de la pregunta.
Los métodos set devolverán una referencia al propio objeto para poder concatenar las asignaciones. Si se hace así, hay que hacerlo de manera explícita, con return this, facilitando la creación de objetos y evitando la necesidad de crear varios constructores con muchos parámetros.
Funciones miembro
A ser posible, los métodos set deben devolver una referencia al propio objeto para poder concatenar las asignaciones. Si se hace así hay que hacerlo de manera explícita, con return this.
Método toString que devuelve el número y el enunciado de la pregunta. Con el siguiente formato: número. Enunciado con la primera en mayúscula.
Método compareTo que compara dos preguntas por su enunciado, tipo de pregunta, dificultad y categoría. Si el enunciado es igual, se comparará por tipo de pregunta, dificultad y categoría.
Sobrescribe los métodos equals y hashCode para que dos preguntas sean iguales si tienen el mismo enunciado, tipo de pregunta, dificultad y categoría (en concordancia con el método compareTo).
3. Clase PreguntaMultiple implanta la interface Predicate<Int>
Clase PreguntaMultiple que hereda de Pregunta e implementa la interfaz Predicate<Int>.
Un predicado es una interfaz funcional con un método que devuelve un valor booleano (test). En este caso, la función test devuelve verdadero si el número de la respuesta correcta es igual al número pasado como parámetro. Por ejemplo, si se llama a test(3) y la respuesta correcta a la pregunta es 3, devolverá verdadero:
var pregunta =new PreguntaMultiple("¿Cuál es la capital de España?");
// ...System.out.println(pregunta.test(3)); //true
Las preguntas multichoice tiene únicamente una lista de tipo Opcion.
Atributos
opciones: lista de preguntas, de tipo Opcion.
Constructores
Un constructor por defecto que inicializa la lista de preguntas.
Uno que recoge la pregunta enunciado, creando la lista.
Funciones miembro
El método set y el método addOpcion/addOpcionesdeben devolver una referencia al propio objeto para poder concatenar las asignaciones. Si se hace así hay que hacerlo de manera explícita, con return this.
addOpcion: recoge una opción (de tipo Opcion) y la añade.
addOpciones: recoge una lista de opciones (de tipo Opcion) y las añade a la lista actual.
get y set para el atributo opciones.
getNumCorrectas: devuelve el número de opciones correctas de la pregunta.
public int getPuntos(List<Integer> marcadas): recoge una lista de enteros con los números de las opciones marcadas (pueden marcar varias) y devuelve los puntos obtenidos. Las incorrectas cuentan negativo. Y ten en cuenta que se considera un punto por pregunta correcta.
Para ello, debes “recorrer” la lista de opciones (marcadas) y comprobar si es correcta o no, llevando cuenta de las correctas y las incorrectas:
Los puntos se calculan con la fórmula:
var puntos = (marcadasBien-marcadasMal)/numCorrectas;
toString: devuelve el enunciado (invoca al toString de la clase padre) y la lista de opciones con el número de opción:
1. ¿Cuál es la capital de España?
a. Madrid
b. Barcelona
c. Sevilla
d. Valencia
Emplea la clase StringBuilder para crear la cadena o una estrategia lo más eficiente posible.
test: implantación del método test de la interfaz. Recoge el Integer y devuelve verdadero si la opción seleccionada es correcta. Comprueba que el valor recogido es un valor válido entre 0 y el número de opciones, además de comprobar que esa opción no es nula (obviamente, en Kotlin esa verificación no es precisa y se realiza de manera más sencilla con el operador ?.)
4. Clase PreguntaVerdaderoFalso
Clase PreguntaVerdaderoFalso que hereda de Pregunta e implementa la interfaz Predicate<Boolean>.
Las preguntas verdadero/falso sólo tiene un booleano que indica si la respuesta es verdadera o falsa.
Atributos
respuesta: booleano que indica si la respuesta es verdadera o falsa.
Constructores
Un constructor por defecto.
Un constructor que recoge el enunciado de la pregunta.
Un constructor que recoge el enunciado de la pregunta y si es correcta o no.
Métodos
toString: devuelve el enunciado (invoca al toString de la clase padre) con las opciones verdadero y falso, marcando la correcta con un asterisco al final de la cadena.
1. ¿La capital de España es Madrid?
a. Verdadero [*]
b. Falso
test: implantación del método test de la interfaz. Recoge el Boolean y devuelve verdadero si la opción seleccionada es correcta.
5. Clase AppTrivial
Esta clase debe crear varias preguntas de trivial y mostrarlas por pantalla.
1. ¿Cuál el pais más extenso del mundo?
a. Rusia
b. Canadá
c. China
d. Estados Unidos
2. ¿Es Kotlin un lenguaje de programación?
a. Verdadero
b. Falso
Conversión a JSON
Emplea la librería Gson para convertir las preguntas a JSON y viceversa, tanto en cadenas como en ficheros.
Hazlo con preguntas tipo test y con preguntas verdadero/falso.
Estudia el resultado mostrado. ¿Es coherente con lo esperado?
Adaptadores de tipo personalizados
1. JsonSerializer
a. Crear un adaptador de tipo personalizado para la enumeración TipoPregunta que ponga el tipo de pregunta como una cadena en minúsculas dentro del objeto JSON de Pregunta. Por ejemplo, si el tipo de pregunta es MULTIPLE, el JSON resultante sería:
{
"tipoPregunta": "multiple"}
Hazlo con expresiones lambda y con una clase anónima.
b. Implementa un adaptador de tipo personalizado para la enumeración TipoPregunta que convierta el tipo de pregunta a un objeto JSON con el siguiente formato:
{
"tipoPregunta": "Multiple"}
c. Realiza el mismo tipo de adaptación que apartado a pero con la enumeración Dificultad.
d. Implementa un adaptador de tipo personalizado, CategoriaAdapter, para la clase Categoria que convierta la categoría a una cadena:
{
"categoria": "General"}
e. Implementa un adaptador de tipo personalizado, PreguntaAdapter, para la clase Pregunta que convierta con el siguiente formato:
{
"type": "multiple",
"difficulty": "easy",
"category": "Programación",
"question": "¿Cuál de los siguientes lenguajes de programación es orientado a objetos puro?",
"options": [
{
"enunciado": "Java",
"correcta": true },
{
"enunciado": "Modula-2",
"correcta": false },
{
"enunciado": "Python",
"correcta": false },
{
"enunciado": "C",
"correcta": false }
]
}
Ayuda: el adaptador de tipo personalizado JsonSerializer para la clase Pregunta debe tener en cuenta que el tipo de pregunta es multiple o boolean y debe instanciar la clase correspondiente. Además, el método serialize debe devolver un objeto JSON con el formato indicado (JsonObjetc objeto = new JsonObject()).
f. Implementa un adaptador de tipo personalizado, PreguntaMultipleAdapter, para la clase PreguntaMultiple que convierta con el siguiente formato:
{
"type": "multiple",
"difficulty": "easy",
"category": "Programación",
"question": "¿Cuál de los siguientes lenguajes de programación es orientado a objetos puro?",
"correct_answer": "Java",
"incorrect_answers": [
"Modula-2", "Python", "C" ]
}
Para facilitar el trabajo de aplicaciones sencillas, existen muchos SGBD relacionales orientados a archivo (embebidos) opensource como H2, SQLite, HSQL, tinySQL, smallSQL o comerciales:
Uno de los SGBD más empleados, sobre todo en dispositivos móviles, es SQLite.
Como trabajaremos con dependencias a los Drivers JDBC, cuyo archivo jar precisamos en nuestro proyecto y en el classpath de ejecución/compilación, recomendaría realizar un proyecto Maven, aunque podría descargarse el driver JDBC de SQLite y añadirse como biblioteca al proyecto Java.
Una de las mejores páginas para consultar información sobre SGBD es:
El hecho de realizarlo en SQLite da portabilidad al proyecto, pues este SGBDR es orientado a archivo y no precisa estar instalado como servicio en ningún computador.
Como dice en la página del proyecto:
“SQLite es una biblioteca ’en proceso’ que implanta un motor de bases de datos SQL autónomo, sin servidor, sin configuración y transaccional…”
“SQLite es el motor de base de datos más utilizado del mundo. SQLite está integrado en todos los teléfonos móviles y la mayoría de las computadoras y viene incluido dentro de innumerables otras aplicaciones que la gente usa todos los días.”
Para la creación de la BD podemos emplear los propios IDE, HeidiSQL, DBeaver (recomendación personal en este caso) o similar. En este caso tan concreto, recomendaría usar DBeaver, pues permite gestionar muchas bases de datos (prácticamente todas las que disponen de Drivers JDBC, pues este programa está escrito en Java):
Por ejemplo, puedes crear la base de datos con de acuerdo con el script SQL o por medio de la interface gráfica (no entraré en detalles):
CREATETABLEPUBLIC.Debuxo (
idDebuxo INTEGER NOTNULL AUTO_INCREMENT,
nome CHARACTER VARYING(64) NOTNULL,
CONSTRAINT DEBUXO_PK PRIMARYKEY (idDebuxo)
);
CREATEINDEX DEBUXO_NOME_IDX ONPUBLIC.DEBUXO (nome);
COMMENTONTABLEPUBLIC.DEBUXO IS'Debuxo da base de datos composto por figuras.';
COMMENTONCOLUMNPUBLIC.DEBUXO. idDebuxo IS'Clave primaria';
COMMENTONCOLUMNPUBLIC.DEBUXO.nome IS'Nome do debuxo';
CREATETABLEPUBLIC.Shape (
idDebuxo INTEGER NOTNULL,
shape BINARY LARGEOBJECT,
CONSTRAINT SHAPE_FK FOREIGNKEY (idDebuxo) REFERENCESPUBLIC.Debuxo(idDebuxo) ONDELETECASCADEONUPDATECASCADE);
CREATEINDEX SHAPE_IDDEBUXO_IDX ONPUBLIC.SHAPE (idDebuxo);
COMMENTONTABLEPUBLIC.shape IS'Figuras de dibujo';
COMMENTONCOLUMNPUBLIC.Shape. idDebuxo IS'Referencia ó debuxo';
COMMENTONCOLUMNPUBLIC.Shape.shape IS'BLOB con el objeto de la figura';
En el ejemplo de BD se emplea un tipo dato BLOB (binario grande), para guardar un objeto binario en la base de datos.
La configuración de la URL a la base de datos es la que aparece en las propiedades de la conexión:
"jdbc:h2:RutaABaseDatos\debuxos"
Los parámetros de la conexión deben ser:
JDBC_DRIVER ="org.h2.Driver"; // No se precisa en JDBC versión mayor a 4.0DB_URL ="jdbc:h2:RutaABaseDatos\nomeBD”;
Ahora podemos proceder como cualquier otro proyecto de conexión a base de datos empleando los dos parámetros (el primero no se precisa desde JDBC 4.0).
02. Introducción a las bases de datos relacionales
1. Introducción a Bases de Datos Relacionales y SQL
Datos son información. Un dato es un hecho, como tu primer nombre. Una base de datos es una colección organizada de datos. En el mundo real, un archivador es un tipo de base de datos. Tiene carpetas, cada una con documentos. Las carpetas se organizan de alguna manera, a menudo alfabéticamente. Cada documento es como un dato. De manera similar, las carpetas en tu computadora son como una base de datos. Las carpetas proporcionan organización y cada archivo es un dato.
Una base de datos relacional está organizada en tablas, que constan de filas y columnas.
Hay dos formas principales de acceder a una base de datos relacional desde Java.
Java Database Connectivity Language (JDBC): Accede a los datos como filas y columnas. JDBC es la API cubierta en este capítulo.
Java Persistence API (JPA): Accede a los datos a través de objetos Java mediante un concepto llamado object-relational mapping (ORM). La idea es que no tienes que escribir tanto código y obtienes tus datos en objetos Java. LO veremos en la unidad de Hibernate, un framework para trabajar con JPA.
Una base de datos relacional se accede mediante Structured Query Language (SQL), un lenguaje de programación utilizado para interactuar con registros de bases de datos. JDBC funciona enviando un comando SQL a la base de datos y luego procesando la respuesta.
Además de las bases de datos relacionales, existe otro tipo de base de datos llamada base de datos NoSQL. Esto es para bases de datos que almacenan sus datos en un formato diferente a las tablas, como almacenes de claves/valores, almacenes de documentos y bases de datos basadas en gráficos. NoSQL lo veremos en unidades siguientes.
En los siguientes apartados, veremos una pequeña base de datos relacional que usaremos en algún ejemplo y veremos las sentencias SQL para acceder a ella.
1.1. Derby
En esta introducción veremos Derby (http://db.apache.org/derby) pero en el aula, para facilitar el trabajo emplearemos SQLite o H2, también haremos ejemplos con MadiaDB.
Derby es una base de datos pequeña en memoria. De hecho, sólo se necesita un archivo JAR para ejecutarlo.
La descarga e implantación es muy sencilla, pero os mostraré cómo hacerlo (podré un enlace al final)
También hay bases de datos “independientes”, orientadas a servicio, que podéis probar par instanalr un motor completo de base de datos. Los más populares (y recomendables) son: MySQL (www.mysql.com), MariaDB (https://mariadb.org/) o PostgreSQL (https://www.postgresql.org/),de código abierto y con más de 20 años de existencia.
Aunque las principales bases de datos tienen muchas similitudes, también tienen diferencias importantes y características avanzadas. Elegir la base de datos correcta para tu trabajo es una decisión importante que debes investigar mucho. Cualquier Sistema Gestor de Bases de Datos es bueno para practicar.
Hay muchos manuales para instalar y comenzar con cualquiera de estos Sistemas Gestores de Bases de Datos (SGBD o BDMS). Está fuera de los contenidos de esta materia configurar una base de datos, pero es algo que se precisa conocer si deseamos implantar nuestras aplicaciones de acceso a datos.
2. Ejemplo de una base de datos relacional
A modo de ejemplo, emplearemos una base de datos con dos tablas. Una tiene una fila por cada especie del zoológico. La otra tiene una fila por cada animal. Estas dos están relacionadas porque un animal pertenece a una especie. Estas relaciones son por las que este tipo de base de datos se llama base de datos relacional.
Tablas en nuestra base de datos relacional:
Especie:
idEspecie (PK)
nome varchar(255)
area (decimal)
1
Elefante Africano
9.5
2
Cebra
3.1
Animal:
idAnimal (PK)
idEspecie (FK)
nome, varchar (255)
dataNacemento (timestamp)
1
1
Pepa
2001-05-06 02:15:00
2
2
Lola
2012-08-15 09:12:00
3
1
Dumbo
2022-09-09 10:36:00
4
1
Babar
2010-06-08 01:24:00
5
2
Rayas
2013-06-08 01:24:00
Hemos declarado dos tablas (después lo ajustaremos un poco), una se llama Especie y la otra Animal. Cada tabla tiene una clave primaria (PK), que nos da una forma única de referenciar cada fila. Después de todo, dos animales pueden tener el mismo nombre, pero no pueden tener la misma ID.
Nota:
En el ejemplo, la clave primaria es sólo una columna. En algunas situaciones, es una combinación de columnas llamada clave compuesta. Por ejemplo, un identificador de estudiante y año podrían ser una clave compuesta. Hay dos filas y tres columnas en la tabla Especie y cinco filas y tres columnas en la tabla Animal.
2.1 Código para configurar la base de datos
En el código SQL se utilizan partes de SQL llamadas lenguaje de definición de base de datos (DDL) y lenguaje de manipulación de datos (DML).
Antes de ejecutar el código, debes agregar un archivo .jar al classpath o añadir la dependencia Maven. Agrega <PATH TO DERBY>/derby.jar al classpath. Asegúrate de reemplazar <PATH TO DERBY> con la ruta real en tu sistema de archivos.
Lo más correcto es introducir la biblioteca de Derby en el directorio db/lib de Java y tener configurado de manera adecuada la variable JAVA_HOME: <JAVA_HOME>/db/lib/derby.jar
El programa se conecta a la base de datos y crea dos tablas. Luego carga datos en esas tablas.
De momento sólo es un ejemplo sencillo, veremos cómo una Connection y un PreparedStatement de varios modos.
3. Repaso de declaraciones SQL básicas
Hay cuatro tipos de operaciones para trabajar con los datos en las bases de datos. Se conocen como CRUD (Crear, Leer, Actualizar, Eliminar). Las palabras clave de SQL no coinciden con el acrónimo:
Tabla 21.1 Operaciones CRUD:
Operación
Palabra Clave SQL
Descripción
Crear
INSERT
Agrega una nueva fila a la tabla
Leer
SELECT
Recupera datos de la tabla
Actualizar
UPDATE
Cambia cero o más filas en la tabla
Eliminar
DELETE
Elimina cero o más filas de la tabla
Si ya conoces SQL, puedes saltar el resto de este apartado. Se muestra lo básico para aquellos que no saben o por si quieres repasar algún concepto básico.
A diferencia de Java, las palabras clave de SQL no distinguen entre mayúsculas y minúsculas. Esto significa que select, SELECT y Select son equivalentes. Muchas personas usan mayúsculas para las palabras clave de la base de datos para que destaquen. También es una práctica común usar snake case (guión bajo para separar “palabras”) en los nombres de las columnas. Tened en cuenta que en algunas bases de datos, los nombres de tabla y columna pueden ser sensibles a mayúsculas y minúsculas.
Al igual que los tipos primitivos de Java, SQL tiene varios tipos de datos. La mayoría son autoexplicativos, como INTEGER. También hay DECIMAL, que funciona de manera similar a un double en Java. El más extraño es VARCHAR, que significa “variable character” y es similar a un String en Java. La parte variable significa que la base de datos debe usar sólo el espacio necesario para almacenar el valor.
La declaración INSERT se utiliza generalmente para crear una nueva fila en una tabla; aquí tienes un ejemplo:
INSERTINTO Especie
VALUES (3, 'Elefante Africano', 10.8);
La declaración INSERT enumera los valores que queremos insertar. De forma predeterminada, utiliza el mismo orden en el que se definieron las columnas. Los datos de cadena están encerrados entre comillas simples.
La declaración SELECT lee datos de la tabla.
SELECT*FROM Especie
WHERE idEspecie =3;
La cláusula WHERE es opcional. Si la omites, se devuelven los contenidos de toda la tabla. El * indica que se devuelvan todas las columnas en el orden en que se definieron. Alternativamente, puedes enumerar las columnas que deseas que se devuelvan.
SELECT nome, area
FROM Especie
WHERE idEspecie =3;
Es preferible enumerar los nombres de las columnas para mayor claridad. También ayuda en caso de que la tabla cambie en la base de datos.
También puedes obtener información sobre todo el resultado sin devolver filas individuales mediante funciones SQL especiales.
SELECTCOUNT(*), SUM(area)
FROM Especie;
Esta consulta nos dice cuántas especies tenemos y cuánto espacio necesitamos para ellas. Devuelve sólo una fila ya que está combinando información (funciones de agregación), Si no hay filas en la tabla, la consulta devuelve una fila que contiene cero como respuesta.
La declaración UPDATE cambia los valores cero o más filas en la base de datos.
UPDATE Especie
SET area = area + .5WHERE nome ='Elefante Asiático';
Nuevamente, la cláusula WHERE es opcional. Si se omite, se actualizarán todas las filas de la tabla. La declaración UPDATE siempre especifica la tabla a actualizar y la columna a actualizar.
La declaración DELETE elimina una o más filas de la base de datos.
DELETEFROM Especie
WHERE nome ='Elefante Asiático';
Y una vez más, la cláusula WHERE es opcional. Si se omite, se vaciará toda la tabla. ¡Así que ten cuidado! ;-)
Todo el SQL mostrado en esta sección es común en casi todos los SGBDR, mas SQL más avanzado, hay variación entre bases de datos, con funciones o tipos de datos diferentes.
Instalación de PostgreSQL sin privilegios de administrador
Muchas veces (instituto u otro centro educativo, organización,…) es muy poco probable que se tengan privilegios de administrador para instalar cualquier software ajeno, por lo que veremos cómo instalar PostgreSQL sin privilegios de administrador en Windows y Linux.
1. Instalación en Windows
Sigue los siguientes pasos para instalar PostgreSQL:
Crea una nueva carpeta en un directorio con control total y extrae estos archivos zip binarios en ella. (Preferiblemente, puedes extraer estos archivos binarios en ubicaciones como la unidad D:\ o E:). La estructura de la carpeta será algo similar a la siguiente:
Agrega esta ubicación del directorio “bin” en la variable PATH en las Variables de entorno de esta cuenta (de usuario).
Ya está instalado en el Sistema Operativo. Ahora, necesitamos configurar la base de datos.
1.1. Verificación de la instalación
Para verificar si está instalado correctamente, usa los siguientes comandos.
El siguiente comando verifica la versión del servidor PostgreSQL:
postgres -V
Obtenemos una salida similar a la siguiente:
postgres (PostgreSQL) 17.4
El siguiente comando verifica la versión del cliente PostgreSQL:
psql -V
Obtenemos una salida similar a la siguiente:
psql (PostgreSQL) 17.4
1.2. Creación de una base de datos y asociar un usuario en PostgreSQL
Para crear una base de datos y asociar un usuario a ésta, la base de datos se inicializará en la ubicación que hemos especificado (carpeta de data en este caso, opción recomendable). La orden es la siguiente:
Debes sustituir la ruta por la que hayas elegido para la carpeta de PostgreSQL.
initdb -D path/to/db/server/ -U NAME -E utf8
Alguna de las opciones que podemos usar son:
-D path/to/db/server/: informa a initdb para inicializar la base de datos( cluster de base de datos) en una ubicación concreta especificada por el usuario. Después de especificar la ubicación, se creará implícitamente un nuevo directorio y se guardarán aquí todos los archivos de PostgreSQL y sus datos relacionados.
-U NAME o –username=NAME: crea un usuario con el nombre especificado y con todos los privilegios de superusuario.
-W:se utiliza para solicitar explícitamente una contraseña para el nuevo superusuario.
-E: indica la codificación que se utilizará para la base de datos.
-A METODO o –auth=MÉTODO: se usa para especificar el cifrado de la contraseña para conexiones locales.
–auth-local=METODO se usa para especificar el cifrado de la contraseña para conexiones locales por socket.
–auth-host=METODO se usa para especificar el cifrado de la contraseña para conexiones de red.
–locale=LOCALE: se usa para especificar la configuración regional.
La salida será similar a la siguiente:
E:\99 - Portables\pgsql>initdb -D .\data -U postgres -E utf8
The files belonging to this database system will be owned by user "pepecalo".
This user must also own the server process.
The database cluster will be initialized with locale "Galician_Spain.1252".
initdb: could not find suitable text search configuration for locale "Galician_Spain.1252"The default text search configuration will be set to "simple".
Data page checksums are disabled.
creating directory data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... windows
selecting default max_connections ... 100selecting default shared_buffers ... 128MB
selecting default time zone ... Europe/Paris
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.
Success. You can now start the database server using:
pg_ctl -D ^"^.^\data^" -l logfile start
1.3. Iniciar el sistema gestor de base de datos
Inicia el sistema gestor de base de datos ejecutando:
E:\99 - Portables\pgsql>pg_ctl -D .\data -l logfile start
waiting for server to start.... doneserver started
El puerto por defecto de PostgreSQL es el5432. Si deseas cambiar el puerto, puedes hacerlo en el archivo postgresql.conf en la carpeta data que hemos creado.
1.4. pgAdmin4
Aunque trabajaremos con Dbeaver, una vez que el servidor PostgreSQL esté en funcionamiento, puedes usar pgAdmin4 para administrar la base de datos. Para abrir pgAdmin4, sigue los siguientes pasos:
Esta es una herramienta de administración de bases de datos para PostgreSQL y derivados, que puede ser instalada para un usuario concreto.
Instala pgAdmin4 en la carpeta de tu elección. Por ejemplo, en la carpeta E:\99 - Portables\pgsql\pgAdmin4.
Haz doble clic en la aplicación pgAdmin4 (ruta E:\99 - Portables\pgAdmin 4\runtime) para iniciar el programa (antes era versión Web pero ahora es versión de escritorio).
Ahora haz clic en Servers en el lado derecho para crear un nuevo servidor para tu base de datos. Completa los detalles requeridos. Crea una conexión con el servidor local en el puerto 5432 y con el usuario postgres:
Haz clic en la sección de Databases para crear una nueva base de datos para tu trabajo y comenzar a usarla.
Para detener la base de datos, utiliza el mismo comando utilizado para iniciar la base de datos como se usó arrancarla y sustituye start por stop.
Para usar funciones adicionales de PGSQL como el vaciado (vacuuming), upgrade, restore, etc., es posible que necesites configurar las rutas binarias para ello. Para esto, ve a File -> Preferences. Ahora desplázate hacia abajo hasta Paths y haz clic en Binary Paths. Especifica las rutas como el directorio de la carpeta bin de instalación de PgSQL:
EDB Advanced Server Binary Path: E:\99 - Portables\pgsql\bin
Nota: La versión puede cambiar, por lo que asegúrate de descargar la última versión.
Descomprime el código fuente con tar xvzf postgresql-17.4.tar.gz.
tar -xzvf postgresql-[version].tar.gz
Navega a la carpeta con cd postgresql-17.4/.
Nota: a veces es necesario tener instaladas las dependencias de compilación de PostgreSQL. Puedes instalarlas con sudo apt-get install build-essential ;-)
Inicializa la base de datos con initdb -D $HOME/postgresql/data.
Inicia la base de datos con pg_ctl -D $HOME/postgresql/data -l $HOME/postgresql/server.log start.
Ahora deberías tener PostgreSQL instalado y ejecutándose en tu sistema Ubuntu sin necesidad de sudo. Puedes acceder a la línea de comandos de PostgreSQL ejecutando psql y conectar a tu servidor PostgreSQL utilizando el usuario por defecto postgres. Por ejemplo:
psql -U postgres
Luego podrás comenzar a trabajar con tu base de datos PostgreSQL.
Puedes cambiar la contraseña con ALTER USER <username> PASSWORD 'new_password_here';.
Puedes eliminar la base de datos predeterminada creada con DROP DATABASE postgres;.
Puedes crear una base de datos con el mismo nombre que la cuenta del sistema operativo con CREATE DATABASE <username>;.
Puedes conectarte a PostgreSQL simplemente usando psql.
3. Instalación en Ubuntu con Docker (sin sudo)
Si no puedes instalar PostgreSQL en tu sistema, puedes usar Docker para ejecutar PostgreSQL en un contenedor.
(Ya instalado en el centro) Instala Docker siguiendo las instrucciones de la documentación oficial.
Ejecuta el siguiente comando para descargar la imagen de PostgreSQL:
docker pull postgres
Ejecuta el siguiente comando para ejecutar PostgreSQL en un contenedor:
docker run --name nombreContendor -e POSTGRES_PASSWORD=micontraseña -p 5432:5432 -d postgres
Conéctate a PostgreSQL ejecutando el siguiente comando:
docker exec -it nombreContendor psql -U postgres
Ahora puedes trabajar con PostgreSQL en tu sistema Ubuntu sin necesidad de instalarlo.
4. Instalación en Windows con Docker
Si no puedes instalar PostgreSQL en tu sistema, puedes usar Docker para ejecutar PostgreSQL en un contenedor.
(Ya instalado en el centro) Instala Docker siguiendo las instrucciones de la documentación oficial.
Ejecuta el siguiente comando para descargar la imagen de PostgreSQL:
docker pull postgres
o emplea la interfaz gráfica de Docker: Docker Desktop:
Pulsando en Pull descargamos la imagen de PostgreSQL.
Ejecuta el siguiente comando para ejecutar PostgreSQL en un contenedor:
docker run --name nombreContenedor -e POSTGRES_PASSWORD=contraseña -p 5432:5432 -d postgres
La opción -p 5432:5432 mapea el puerto 5432 del contenedor al puerto 5432 del host.
La opción -e POSTGRES_PASSWORD=contraseña establece la contraseña de la base de datos.
La opción --name nombreContenedor establece el nombre del contenedor.
La opción -d ejecuta el contenedor en segundo plano con el modo demonio.
Postgres estará ejecutándose en el contenedor en el puerto 5432.
Si lo haces desde Docker Desktop, puedes hacer clic en Run para ejecutar el contenedor y verlo en la pestaña Containers/Apps. Debes poner la variable de entorno POSTGRES_PASSWORD con la contraseña que desees.
Conéctate a PostgreSQL ejecutando el siguiente comando:
docker exec -it nombreContenedor psql -U postgres
También puedes usar pgAdmin4 para administrar la base de datos. Para ello, sigue los pasos anteriores para instalar pgAdmin4 en Windows o el Dbveaver.
Ahora puedes trabajar con PostgreSQL en tu sistema Windows sin necesidad de instalarlo.
PostgreSQL es un sistema de gestión de bases de datos objeto-relacional potente y de código abierto que tiene como objetivo ayudar a los desarrolladores a construir aplicaciones y a los administradores a proteger la integridad de los datos y construir entornos tolerantes a fallos. Admite tipos de datos avanzados y características de optimización de rendimiento, como Ms-SQL Server y Oracle.
1. Características de PostgreSQL
Sistema de gestión de bases de datos de código abierto
Admite propiedades ACID
Técnicas de indexación diversas
Replicación basada en registros y basada en disparadores SSL
Soporte para JSON
Admite objetos geográficos
Compatible con orientado a objetos y ANSI-SQL 2008
2. Tipos de Datos en PostgreSQL
Numeric
Character
Date/Time
Monetary
Binary
Boolean
Geométrico
JSON
Enumerado
Búsqueda de texto
UUID
Tipos de dirección de red
Compuesto
Identificadores de objeto
Pseudo
BitString
XML
Rango
Arrays
pg_lsn
Datos Numéricos: smallint, integer, bigint, decimal, numeric, real, serial.
Datos de Carácter: varchar(n), text, char(n).
Datos de Fecha/Hora: timestamp, date, time, interval.
Tipo de Datos Monetarios: money.
Tipo de Datos Binarios: bytea (admite formato hexadecimal y de escape).
Tipo de Datos Booleano: boolean.
Tipos de Datos Geométricos: point, line, box, path, polygon, circle, lseg.
Tipos de Datos JSON: string, number, boolean, null.
Tipos de Datos Enumerados: enum.
Tipo de Datos UUID: uuid (almacena Identificadores Únicos Universales).
Tipos de Datos de Rango: int4range, int8range, numrange, tsrange (rango de marcas de tiempo), daterange.
Tipo de Datos pg_lsn: pg_lsn (almacena Número de Secuencia de Registro).
3. Operadores en PostgreSQL
Un operador manipula elementos de datos individuales y devuelve un resultado. Estas son las palabras reservadas utilizadas en la cláusula WHERE para realizar operaciones.
Operadores Aritméticos: +, -, *, /, %, ^, !
Operadores de Comparación: =, !=, <>, >, <, >=, <=
Operadores Lógicos: AND, NOT, OR
Operadores a Nivel de Bits: &, |
4. Instalación en Linux
a) Para instalar PostgreSQL, ejecuta el siguiente comando:
sudo apt install postgresql
O
sudo apt install postgresql postgresql-contrib
postgresql-contrib agregará algunas utilidades y funcionalidades adicionales.
b) Después de la instalación, cambia a la cuenta de Postgres:
sudo -i -u postgres
c) Ahora, puedes acceder al prompt de Postgres usando el comando psql.
5. Trabajando con Bases de Datos:
CREATE DATABASE: Se utiliza para crear la base de datos.
Es posible modificar la estructura de una tabla existente utilizando la instrucción ALTER TABLE. PostgreSQL admite diversas acciones para realizar con ALTER TABLE, que se enumeran a continuación:
UPDATE: Se utiliza para actualizar o modificar datos existentes en la tabla.
UPDATE nombre_tabla
SET columna_1 = valor_1,
columna_2 = valor_2, ...
WHEREcondición_1 AND condición_2;
DELETE: Se utiliza para eliminar fila(s) de la tabla.
DELETEFROM nombre_tabla
WHERE condición;
7. Restauración mediante la línea de comandos
Para restaurar una base de datos mediante la línea de comandos, seguiremos el siguiente procedimiento:
En primer lugar, necesitamos iniciar sesión en el terminal de PostgreSQL a través de la línea de comandos. Para hacerlo, escriba el siguiente comando:
psql -U <nombre_usuario>
Ahora podemos ver que hemos iniciado sesión correctamente en el terminal cliente psql y hemos obtenido el indicador de entrada de línea de comandos de PostgreSQL.
Lo haremos esta vez a través del terminal de línea de comandos de PostgreSQL.
Ahora crearemos una base de datos de marcador de posición para nuestro propósito que se utilizará para restaurar la copia de seguridad. Para hacerlo, ejecute el siguiente script.
La base de datos ahora está creada. Ahora vamos a restaurarla. Para restaurar la base de datos, vamos a utilizar el comando pg_restore suministrado con algunos argumentos. Es importante tener en cuenta aquí que necesitamos salir del terminal psql para poder ejecutar el comando pg_restore. Para salir del terminal psql, escriba “\q”.
Introduce el comando pg_restore con los siguientes argumentos:
La explicación detallada de los argumentos para PostgreSQL se puede encontrar en el sitio web oficial de PostgreSQL en la sección de documentación de pg_restore.
Después de la restauración exitosa de la base de datos, veremos que nuestros esquemas se han restaurado junto con las tablas y sus datos.
02.03. Procesamiento de sentencias SQL.
En este apartado veremos las interfaces y clases que declara el API JDBC.
Subsecciones de 02.03. Procesamiento de sentencias SQL.
La API Java JDBC (Java Database Connectivity) permite que las aplicaciones Java se conecten a SGBD relacionales.
La API JDBC permite consultar y actualizar, así como procedimientos almacenados u obtener metadatos sobre la base de datos relacionales (como MySQL, PostgreSQL, MS SQL Server, Oracle, H2 Database, etc.)
La API Java JDBC forma parte del SDK, por lo que está disponible para todas las aplicaciones Java.
Java proporciona conexión a bases de datos mediante JDBC (Java Database Connection) que proporciona una interface a muchos tipos de bases de datos.
Oculta las diferencias debajo de SQL y proporciona un conjunto de interfaces que son una abstracción de la funcionalidad de la base de datos.
Nos conectamos desde java con unos controladores (drivers), implementaciones de las interfaces JDBC del API que pueden haber sido escritos en puro Java, para ser 100% portables, o pueden implicar un componente nativo. Un ejemplo es el puente JDBC-ODBC, que depende del S.O. y sólo se puede ejecutar en Windows.
Características
JDBC es independiente del Sistema Gestor de BD.
JDBC no es independiente de SQL: el dialecto de SQL utilizado por diferentes bases de datos varía ligeramente dependiente del SGBD (emplea SQL estándar)
NO es para SGBD no relacionales como MongoDB, Cassandra, Dynamo, etc, que tienen su propia biblioteca Java.
El paquete principal es java.sql, pero desde el API JDBC 4.3 se incluye tanto el java.sql, denominado API principal de JDBC, como el javax.sql, denominado API del paquete opcional JDBC. Esta API JDBC completa está incluida en Java Standard Edition (Java SE), desde la versión 7.
java.sql: API principal para acceder y procesar datos almacenados en una fuente de datos (normalmente una base de datos relacional) utilizando el lenguaje de programación Java. Incluye un marco mediante el cual se pueden instalar diferentes controladores de forma dinámica para acceder a diferentes fuentes de datos. Aunque la API JDBC está diseñada principalmente para pasar declaraciones SQL a una base de datos, permite leer y escribir datos de cualquier fuente de datos con formato tabular. Existen unas interfaces de lectura/escritura en javax.sql.RowSet, que se puede personalizar para usar y actualizar datos de una hoja de cálculo, un archivo plano o cualquier otra fuente de datos tabulares. Incluye:
Clases e interfaces para establecer conexiones a BBD con la clase DriverManager: DriverManager, SQLPermission, Driver, DriverPropertyInfo.
Envío de sentencias a la base de datos: Statement, PreparedStatement, CallableStatement, Connection, Savepoint.
Obtención y actualización de resultado s de una consulta: ResultSet.
Mapeo de tipos SQL a clases e interfaces Java: Array, Blob, Clob, Date, NClob, Ref, RowId, Struct, SQLXML, Time, Timestamp, Types.
Mapeo personalizado de tipos SQL definidos por el usuario (UDT) a clases Java: SQLDat, SQLInput, SQLOuput.
javax.sql: API para el acceso y procesamiento de fuentes de datos del lado del servidor desde el lenguaje de programación Java. Complementa el paquete java.sql y, a partir de la versión 1.4, se incluye en Java Platform, Standard Edition (Java SE). Sigue siendo una parte esencial de Java Platform, Enterprise Edition (Java EE).
Los paquetes relacionados:
API para transacciones distribuidas: javax.transaction.xa, define el contrato entre el gestor de transaccione y el gestor recursos, lo que permite al administrador de transacciones dar de alta y eliminar objetos de recursos (proporcionados por el controlador del administrador de recursos) en transacciones JTA.
java.util.logging: proporciona las clases e interfaces de las funciones principales de log de la plataforma Java 2. El objetivo principal del API de logging es respaldar el mantenimiento y servicio del software en los sitios de los clientes.
Módulo java.xml: declara y define el API de Java para procesamiento XML (JAXP = Java APIs for XML Processing).
Etapas de procesamiento de sentencias
En general, para procesar cualquier sentencia SQL con JDBC, sigue estos pasos:
Establecer una conexión (Connection)
Crea una declaración (Statement)
Ejecuta la consulta (executeQuery/executeUpdate/execute)
Procesa el objeto ResultSet, en el caso de ser de consulta)
Cierra la conexión (de manera automática con try-catch-with-resources)
Por ejemplo, el método, Juego.showTabla el contenido de la tabla Juego:
publicstaticvoidshowTabla(Connection con) throws SQLException {
String query ="select nombre, idDesarrollador, precio, ventas, total from Juego";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String nombreJuego = rs.getString("nombre");
int idDesarrollador = rs.getInt("idDesarrollador");
float precio = rs.getFloat("precio");
int ventas = rs.getInt("ventas");
int total = rs.getInt("total");
System.out.println(nombreJuego +", "+ idDesarrollador +", "+ precio +", "+ ventas +", "+ total);
}
} catch (SQLException e) {
// Gestión de la excepción. }
}
1. Establecimiento Connection
Primero, establece una conexión con la fuente de datos que deseas utilizar. Una fuente de datos (Data source) puede ser un sistema de gestión de bases de datos (DBMS), un sistema de archivos heredado u otra fuente de datos con un controlador JDBC correspondiente. Esta conexión está representada por un objeto Connection.
2. Creación de Statement
Un Statement es una interfaz que representa una sentencia SQL.
Si se invocan métodos de consulta (executeQuery) sobre un Statement y generan objetos ResultSet, una tabla de datos que representa un conjunto de resultados de base de datos.
Por ejemplo, Juego.showTabla crea un objeto Statement con el siguiente código:
stmt = con.createStatement();
Existen tres tipos diferentes de declaraciones:
Statement: Se utiliza para implementar sentencias SQL simples sin parámetros.
PreparedStatement: (hereda de Statement.) Se utiliza para compilar previamente (precompilar) sentencias SQL que pueden contener parámetros de entrada.
CallableStatement: (hereda de PreparedStatement.) se utiliza para ejecutar procedimientos almacenados que pueden contener parámetros de entrada y salida.
3. Ejecución de consultas: execute, executeQuery, executeUpdate
Para ejecutar una consulta, llama a un método de tipo execute de Statement. Existen 3 versiones:
execute: devuelve true si el primer objeto que devuelve la consulta es un objeto ResultSet. Se utiliza este método si la consulta podría devolver uno o más objetos ResultSet. Después se recuperan los objetos ResultSet devueltos por la consulta llamando repetidamente a Statement.getResultSet.
executeQuery: devuelve un objeto ResultSet.
executeUpdate: devuelve un entero que representa el número de filas afectadas por la sentencia SQL, con sentencias SQL INSERT, DELETE o UPDATE.
Por ejemplo, Juego.showTabla ejecutó un objeto Statement con el siguiente código:
ResultSet rs = stmt.executeQuery(query);
4. Obtención de objetos ResultSet
Para acceder a los datos de un objeto ResultSet se realiza a través un cursor, que no es un cursor de la base de datos. Es un puntero que apunta a una fila de datos en el objeto ResultSet. Inicialmente, el cursor se encuentra antes de la primera fila.
Existe varios métodos definidos en el objeto ResultSet para mover el cursor (next, previous,…)
Por ejemplo, Juego.showTabla llama repetidamente al método ResultSet.next para mover el cursor hacia adelante una fila. Cada vez que llama a next, el método obtiene los datos en la fila donde se encuentra actualmente el cursor:
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String nombreJuego = rs.getString("nombre");
int idDesarrollador = rs.getInt("idDesarrollador");
float precio = rs.getFloat("precio");
int ventas = rs.getInt("ventas");
int total = rs.getInt("total");
System.out.println(nombreJuego +", "+ idDesarrollador +", "+ precio +", "+ ventas +", "+ total);
}
// ...
5. Cierre de Conexiones
Siempre que no precisemos más los objetos Connection, Statement o ResultSet, llama a su método close para liberar inmediatamente los recursos que está utilizando.
Es mejor recomendación emplear una declaración try-with-resources para cerrar automáticamente los objetos Connection, Statement y ResultSet, independientemente de si ha lanzado una SQLException. (JDBC lanza una SQLException cuando encuentra un error durante una interacción con una fuente de datos.)
Como sabes, una declaración automática de recursos consta de una declaración try y uno o más recursos declarados. Por ejemplo, el método Juego.showTabla cierra automáticamente su objeto Statement, de la siguiente manera:
publicstaticvoidviewTable(Connection con) throws SQLException {
String query ="select nombre, idDesarrollador, precio, ventas, total from Juego";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String nombreJuego = rs.getString("nombre");
int idDesarrollador = rs.getInt("idDesarrollador");
float precio = rs.getFloat("precio");
int ventas = rs.getInt("ventas");
int total = rs.getInt("total");
System.out.println(nombreJuego +", "+ idDesarrollador +", "+ precio +", "+ ventas +", "+ total);
}
} catch (SQLException e) {
JDBCTutorialUtilities.printSQLException(e);
}
}
La siguiente declaración es una declaración try-with-resources, que declara un recurso, stmt, que se cerrará automáticamente cuando el bloque try finalice:
(El usuario de la base de datos es el usuario por defecto, “sa”, sin comillas, y la contraseña en blanco)
Para permitir el uso de nombres en CamelCase en H2 JDBC Driver versión 2, es necesario agregar la propiedad DATABASE_TO_UPPER=FALSE en la URL de conexión.
Por ejemplo, si la URL de conexión es jdbc:h2:/test, la URL de conexión con la propiedad DATABASE_TO_UPPER=FALSE sería *jdbc:h2:/test;DATABASE_TO_UPPER=FALSE*.
URL: jdbc:h2:ruta_a_baseDatos/JuegosH2
Ejercicio. Crear y transferir datos JSON-BD
Descarga de datos JSON y almacenamiento en una base de datos SQLite. Para ello debes ampliar el ejercicio anterior de JSON.
Al menos debes haber realizado los adaptadores de tipo, las clases del modelo para poder realizar de manera mejor diseñada el apartado i), que hace referencia a BD. Si no está hecho, debes leer el JSON y guardar los datos en la base de datos SQLite creada, con las tablas:
Plataforma: idPlataforma, nombre.
Genero: idGenero, nombre.
Juego: idJuego, titulo, miniatura (varchar), estado, descripción, url, idGenero (FK), idePlataforma (FK), editor, desarrollador, fecha, urlFreeToGame.
Imagen: idImagen, idJuego (FK), url, imagen (tipo BLOB). De momento, sólo se guardará la URL a la imagen.
La ordenación puede ser: release-date, popularity, alphabetical o relevance
{
"id": 452,
"title": "Call Of Duty: Warzone",
"thumbnail": "https:\/\/www.freetogame.com\/g\/452\/thumbnail.jpg",
"status": "Live",
"short_description": "A standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare.",
"description": "Call of Duty: Warzone is both a standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare. Warzone features two modes \u2014 the general 150-player battle royle, and \u201cPlunder\u201d. The latter mode is described as a \u201crace to deposit the most Cash\u201d. In both modes players can both earn and loot cash to be used when purchasing in-match equipment, field upgrades, and more. Both cash and XP are earned in a variety of ways, including completing contracts.\r\n\r\nAn interesting feature of the game is one that allows players who have been killed in a match to rejoin it by winning a 1v1 match against other felled players in the Gulag.\r\n\r\nOf course, being a battle royale, the game does offer a battle pass. The pass offers players new weapons, playable characters, Call of Duty points, blueprints, and more. Players can also earn plenty of new items by completing objectives offered with the pass.",
"game_url": "https:\/\/www.freetogame.com\/open\/call-of-duty-warzone",
"genre": "Shooter",
"platform": "Windows",
"publisher": "Activision",
"developer": "Infinity Ward",
"release_date": "2020-03-10",
"freetogame_profile_url": "https:\/\/www.freetogame.com\/call-of-duty-warzone",
"minimum_system_requirements": {
"os": "Windows 7 64-Bit (SP1) or Windows 10 64-Bit",
"processor": "Intel Core i3-4340 or AMD FX-6300",
"memory": "8GB RAM",
"graphics": "NVIDIA GeForce GTX 670 \/ GeForce GTX 1650 or Radeon HD 7950",
"storage": "175GB HD space" },
"screenshots": [
{
"id": 1124,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-1.jpg" },
{
"id": 1125,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-2.jpg" },
{
"id": 1126,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-3.jpg" },
{
"id": 1127,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-4.jpg" }
]
}
a) Crea las clases de la aplicación:
Image: con identificador, URL y ¡un array de bytes con la imagen!.
Plataforma: enumeración con 3 posibles valores, BROWSER, PC, ALL.
Game: con identificador, título, miniatura (tipo Image), descripción, url para jugar, género, plataforma (de tipo Plataforma), fecha de realización (LocalDate) y una lista de imágenes.
BrowserGame: hereda de Game y se trata de un juego para navegador, por lo que su categoría será BROWSER.
b) Haz una sencilla aplicación que, a partir de el JSON de un Game, cree un juego, pero sólo el identificador, el título, la descripción, la URL, … sin miniatura ni la lista de imágenes.
c) Haz que el juego se pueda guardar en un archivo de texto con el nombre: nombre del juego.txt y la versión toString del Game dentro de él. Emplea Java IO, no Files.
d) Como sólo nos interesan los juegos de navegador para jugar en clase mientras Pepe explica, haremos una aplicación que descargue la lista de juegos de:
Empleando un InstanceCreator para que asigne la plataforma BROWSER al constructor de Game.
e) Amplía el ejercicio anterior para que también recupere las imágenes, sin los bytes, sólo la url. La miniatura tendrá siempre id igual a 0.
f) Amplía el ejercicio apartado anterior para que guarde recupere también la imagen y la almacene en el array de bytes. Ved nota [1]
g) Usando el API haz una aplicación que pida un identificador de objeto y lo descargue, tanto en un fichero de texto como las imágenes. Previamente debe “deserializar el objeto en el tipo Game”.
h) Si deseas hacer una aplicación gráfica, puedes ver la nota 2, en la que explico cómo crear un ImageIcon a partir de una array de bytes.
i) Haz diseña una base de datos SQLite con la estructura de los datos del JSON y crea un aplicación que descargue los juegos y los guarde en la base de datos.:
Crea la base de datos e introduce los datos “estáticos” de la Plataforma y el Género.
Realiza un programa que lea los archivos JSON, de la URL: https://www.freetogame.com/api/game?id=X, pasándole el id del Juego, desde 1 al número de juegos que consideres. Ten en cuenta que el juego podría no existir devolviendo:
{"status":0,"status_message":"No game found with that id"}
Lee los datos del JSON, por medio de un JsonReader (o un JsonParser) y guárdalos en la base de datos para cada juego, teniendo en cuenta que debes realizar los pasos que hemos comentado:
Establecer conexión con DriverManage.getConnection.
Crear sentencia, Statement.
Ejecutar sentencia de tipo executeUpdate(INSERT INTO …).
Para evitar problemas con los caracteres especiales, comillas, etc. usa como base el siguiente ejemplo:
// Se supone que ya hemos creado la conexión y creamos una sentencia preformateada.// Las ? son los parámetros de la sentencia.PreparedStatement ps= conexion.prepareStatement("UPDATE Filosofo set nome=? , apelidos=? where idFilosofo=?"); // Sólo se realiza al principio y luego se reutiliza para cada inserción o actualización. CON INSERT sería igual, cambiando la sentencia.// asignamos los parámetros a la consulta. En nuestro caso serían los valores a insertar en las tablas.ps.setString(1, "Ludwig Josef Johann");
ps.setString(2, "Wittgenstein");
ps.setLong (3, 1);
int filasAfectadas = ps.executeUpdate(); // típicamente devolverá 1 o 0.// Dentro del bucle podemos volver a insertar nuevos valores sin tener que crear una nueva sentencia.ps.setString(1, "Bertrand Arthur William");
ps.setString(2, "Russell");
ps.setLong (3, 1);
int filasAfectadas = ps.executeUpdate();
Nota 1. Guardar una imagen en un array de bytes:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
publicclassImageToBytes {
publicstaticvoidmain(String[] args) {
try {
// 1. Crea un objeto fis de tipo InputStream a la imagen.// Ya deberías saberlo// 2. Crea un flujo de salida a un array de bytes: ByteArrayOutputStream bos =new ByteArrayOutputStream();
byte[] buf =newbyte[1024]; // bufferfor (int readNum; (readNum = fis.read(buf)) !=-1;) {
// Escribimos en el array de bytes bos.write(buf, 0, readNum);
}
// Convertimos el flujo de bytes en un arraybyte[] bytes = bos.toByteArray();
System.out.println("Imagen convertida a bytes: "+ bytes);
} catch (IOException e) {
// … }
}
}
Nota 2. ImageIcon a partir de un array de bytes:
import java.io.FileOutputStream;
import java.io.IOException;
publicclassBytesToImageFile {
publicstaticvoidmain(String[] args) {
try {
byte[] bytes =newbyte[] { 0x00, 0x01, 0x02, …};
// Ya sabes que hay mejores maneras de crear flujos, más // eficientes, pero a modo de ejemplo. FileOutputStream fos =new FileOutputStream("ruta/a/tu/imagen.jpg");
fos.write(bytes);
fos.close(); // mejor, try-catch-with-resources. System.out.println("Imagen guardada en disco.");
} catch (IOException e) {
// .. }
}
}
Como se ha comentado anteriormente, la fuente de datos puede ser cualquier fuente que tenga un controlador JDBC: un sistema de gestión de bases de datos (DBMS), un sistema de archivos heredado u otra fuente de datos.
Existen dos clases principales para establecer la conexión:
DriverManager: clase totalmente implementada que conecta una aplicación a una fuente de datos, indicada por medio de una URL de base de datos.
Cuando esta clase intenta establecer una conexión por primera vez, carga automáticamente cualquier controlador JDBC 4.0 (o superior) encontrado dentro del classpath. Se precisa cargar de manera manual cualquier controlador con JDBC inferior a 4.0
DataSource:: interfaz se prefiere sobre DriverManager porque permite que los detalles sobre la fuente de datos subyacente sean transparentes para tu aplicación. Las propiedades de un objeto DataSource se configuran para que represente una fuente de datos concreta. Es el modo de conexión preferido para aplicaciones Java EE.
DataSource
En principio emplearemos la clase DriverManager en lugar de la clase DataSource porque es más fácil de usar y, en general, las aplicaciones cliente multiplataforma no requieren las características avanzadas de la clase DataSource que inicialmente estaban diseñadas para ser empleadas en aplicaciones Java EE.
1. La clase DriverManager
Para establecer la conexión con la clase DriverManager sew invoca al método DriverManager.getConnection. El siguiente método de ejemplo establece una conexión de base de datos:
// Suponemos que este método está declarado dentro de una clase ConnectionManager que tiene// - Un atributo sxbd de tipo String, para guardar el nombre del SGBD.// - Un atributo con el nombre del servidor, nomeServidor// - Un atributo con el número de puerto.// - Un atributo para el usuario y otro para la contraseña.public Connection getConnection() throws SQLException {
Connection conn =null;
// Propiedades de la conexión Properties propiedadesCon =new Properties();
propiedadesCon.put("user", this.userName);
propiedadesCon.put("password", this.password);
if (this.sxbd.equals("mysql")) {
conn = DriverManager.getConnection("jdbc:"+this.sxbd+"://"+this.nomeServidor+":"+this.puerto+"/", propiedadesCon);
} elseif (this.sxbd.equals("derby")) {
conn = DriverManager.getConnection("jdbc:"+this.sxbd+":"+this.dbName+";create=true", propiedadesCon);
}
System.out.println("Conexión establecida con la BD");
return conn;
}
El método DriverManager.getConnection(...) establece una conexión de base de datos. Este método requiere una URL de base de datos, que varía según el SGBD/DBMS.
El método devuelve un objeto Connection, que representa una conexión con el DBMS o una base de datos específica. Consulta la base de datos a través de este objeto.
Por ejemplo:
MySQL:jdbc:mysql://localhost:3306/, donde localhost es el nombre del servidor que aloja a la base de datos y 3306 es el número de puerto.
Java DB:jdbc:derby:testdb;create=true, donde testdb es el nombre de la base de datos a la que conectarse y create=true informa al SGBD a crear la base de datos.
Nota:esta URL establece una conexión de base de datos con el controlador Java DB Embedded. Java DB también incluye un controlador Network Client, que utiliza una URL diferente.
El método anterior especifica el nombre de usuario y la contraseña necesarios para acceder al DBMS con un objeto Properties.
Cuando se usa un driver de un proveedor, la documentación informará del subprotocolo que usa, esto es, que pone después de “jdbc:subprotocolo:….” en la URL.
Generalmente, en la URL de la base de datos, también especifica el nombre de una base de datos existente a la que deseas conectarte.Por ejemplo: jdbc:mysql://localhost:3306/jugadores representa la URL de conexión a una base de datos MySQL llamada “jugadores”. En los caso de bases de datos en memoria no se especifica, porque debe ser creada previamente.
En versiones anteriores de JDBC a 4.0, para obtener una conexión, había que cargar previamente el Drive invocando al método estático Class.forName, que debía recoger un objeto de tipo java.sql.Driver.
Cada Driver JDBC contiene una o más clases que implementan la interfaz java.sql.Driver. Los controladores para Java DB son org.apache.derby.jdbc.EmbeddedDriver y org.apache.derby.jdbc.ClientDriver, y el de MySQL Connector/J es com.mysql.cj.jdbc.Driver. En los ejemplo anteriores hemos visto cómo se denominan las implementaciones para otros SGBD que emplearemos durante esta unidad y parte del curso.
Cualquier Driver JDBC 4.0 o superior que se encuentre en el classpath (o en las implementaciones o dependencias del proyecto Java) se carga automáticamente. (Sin embargo, debe cargarse de manera explícita cualquier Driver anterior a JDBC 4.0 con el método Class.forName).
2. Especificando URL de Conexión a la Base de Datos
Una URL de conexión de base de datos es una cadena que el controlador JDBC utiliza para conectarse a una base de datos. Puede contener información sobre dónde buscar la base de datos, el nombre de la base de datos a la que conectarse y propiedades de configuración.
La sintaxis exacta de una URL de conexión de base de datos depende del SGBD:
Java DB Database Connection URLs: Derby
La siguiente es la sintaxis de la URL de conexión de base de datos para Java DB:
subsubprotocol especifica dónde Java DB debe buscar la base de datos, ya sea en un directorio, en memoria, en un classpath o en un archivo JAR. Típicamente, se omite.
databaseName es el nombre de la base de datos a la que conectarse.
attribute=value representa una lista opcional separada por punto y coma de atributos. Estos atributos permiten informar al SGBD de los parámetros de conexión, incluyendo la:
Creación de la base de datos indicada en la URL.
Encriptación de la base de datos.
Indicar directorios para almacenar información de log y traza.
Indicar el nombre de usuario y contraseña para conectarse a la base de datos.
Referencias:
Java DB es un distribución de la base de datos Open Source Apache Derby.
Desde el 2015, JAvaDB no se incluye en JDK y fue eliminado de JDK 7 y 8 el 17 de julio del 2018.
JavaDB ha sido redirigido a Apache Derby, para emplear JavaDB debe usarse la versión del Proyecto Apache Derby.
host:port es el nombre de host y el número de puerto de la computadora que aloja la base de datos. Si no se especifica, los valores predeterminados de host y puerto son 127.0.0.1 y 3306, respectivamente.
database es el nombre de la base de datos a la que conectarse. Si no se especifica, se realiza una conexión sin una base de datos predeterminada.
failover es el nombre de una base de datos en espera (MySQL Connector/J admite failover).
propertyName=propertyValue representa una lista opcional separada por ampersand de propiedades. Estas propiedades te permiten informar a MySQL Connector/J la realización de varias tareas y configuraciones.
Ejemplo con MariaDB:
El conector más reciente de MariaDB es compatible con MySQL 5.5.3 o superior, además de con MariaDB.
Se ajustan a las especificaciones de JDBC 4.2.
Class.forName("org.mariadb.jdbc.Driver"); // Sigue funcionando pero no se precisa.Connection connection = DriverManager.getConnection("jdbc:mariadb://localhost:3306/DB?user=root&password=myPassword");
Todos los parámetros de conexión pueden consultarse en:
Cuando se usa un driver de un proveedor, la documentación informará del subprotocolo que usa, esto es, que pone después de “jdbc:subprotocolo:….” en la URL.
Cuando JDBC encuentra un error durante una interacción con una base de datos a la que está conectado un objeto Connection, lanza una instancia de SQLException.La instancia de SQLException contiene la siguiente información que facilita encontrar la causa del error:
Una descripción del error. Recupera el objeto String que contiene esta descripción llamando al método SQLException.getMessage().
Un código SQLState: códigos y significados estandarizados por ISO/ANSI y Open Group (X/Open), aunque algunos códigos se han reservado para que los definan los proveedores de bases de datos. Este objeto String consta de cinco caracteres alfanuméricos. Recupera este código llamando al método SQLException.getSQLState().
Un código de error: valor entero que identifica el error que causó que la instancia de SQLException se lanzara. Su valor y significado son específicos de la implementación y podrían ser el código de error real devuelto por la fuente de datos. Recupera el error llamando al método SQLException.getErrorCode().
Una causa. Una instancia de SQLException podría tener una relación causal, que consiste en uno o más objetos Throwable que causaron que la instancia de SQLException se lanzara. Para navegar por esta cadena de causas, llama recursivamente al método SQLException.getCause() hasta que se devuelva un valor nulo.
Una referencia a excepciones encadenadas. Si ocurren más de un error, las excepciones se referencian a través de esta cadena. Recupera estas excepciones llamando al método SQLException.getNextException en la excepción que se lanzó.
1. Captura de excepciones
El siguiente método, printSQLException, muestra el SQLState, el código de error, la descripción del error y la causa (si la hay) contenidos en SQLException, así como cualquier otra excepción encadenada:
publicstaticvoidprintSQLException(SQLException ex) {
for (Throwable e : ex) {
if (e instanceof SQLException) {
// Método implantado más adelante:if (!ignoraSQLException(((SQLException)e).getSQLState())) {
// e.printStackTrace(System.err); System.err.println("Estado SQL: "+ ((SQLException)e).getSQLState());
System.err.println("Código error: "+ ((SQLException)e).getErrorCode());
System.err.println("Mensaje: "+ e.getMessage());
Throwable t = ex.getCause();
while(t !=null) {
System.out.println("Causa: "+ t);
t = t.getCause(); // LLamada recursiva. }
}
}
}
}
Por ejemplo, si se invoca una llamada da una tabla que no existe la llamada al método ignoraSQLException, la salida será similar a la siguiente:
Estado SQL: 42Y55
Código error: 30000
Mensaje: 'DROP TABLE' cannot be performed on
'TESTDB.TABLAPRUEBA' because it does not exist.
En lugar de imprimir información de SQLException, podrías en su lugar primero recuperar el SQLState y procesar SQLException en consecuencia. Por ejemplo, el método ignoraSQLException devuelve true si el SQLState es igual al código 42Y55 (y estás utilizando Java DB como tu DBMS), lo que provoca que printSQLException ignore la SQLException:
publicstaticbooleanignoraSQLException(String sqlState) {
if (sqlState ==null) {
System.out.println("Este estado SQL no está declarado!");
returnfalse;
}
// X0Y32: Jar file already exists in schemaif (sqlState.equalsIgnoreCase("X0Y32"))
returntrue;
// 42Y55: Table already exists in schemaif (sqlState.equalsIgnoreCase("42Y55"))
returntrue;
returnfalse;
}
2. Recuperación de warnings
Los objetos SQLWarning son una subclase de SQLException que gestiona los warnings de acceso a la base de datos.
Los warnings no detienen la ejecución de una aplicación, como lo hacen las excepciones; simplemente alertan al usuario de que algo no sucedió según lo planeado. Por ejemplo, una advertencia podría informar que un privilegio que intentaste revocar no se revocó. O una advertencia podría decir que ocurrió un error durante una desconexión solicitada.
Una advertencia se puede informar en un objeto Connection, un objeto Statement (incluidos los objetos PreparedStatement y CallableStatement) o un objeto ResultSet.
Cada una de estas interfaces (y sus clasesimplementadas) tiene un método getWarnings(), que se debe invocar para ver la primera advertencia informada en el objeto que llama.
Si getWarnings devuelve una advertencia, se llamar al método getNextWarning de SQLWarning en ella para obtener cualquier advertencia adicional. La ejecución de una instrucción borra automáticamente las advertencias de una instrucción anterior, por lo que no se acumulan.
Si se desea recuperar advertencias informadas en una orden, debe hacerse antes de ejecutar otra instrucción de cierre.
Por ejemplo, para acceder a cualquier advertencia informada en objetos Statement o ResultSet:
La advertencia más común es una advertencia de DataTruncation, una subclase de SQLWarning. Todos los objetos DataTruncation tienen un SQLState de 01004, lo que indica que hubo un problema al leer o escribir datos.
Los métodos de DataTruncation te permiten averiguar en qué columna o parámetro se truncaron los datos, si fue en una operación de lectura o escritura, cuántos bytes deberían haberse transferido y cuántos bytes se transfirieron realmente.
3. SQLExceptions categorizadas
Tu controlador JDBC podría lanzar una subclase de SQLException que corresponde a un SQLState común o a un estado de error común que no está asociado con un valor de clase SQLState específico, permitiendo concretar la excepción que produjo error:
Las siguientes subclases de SQLException también pueden lanzarse:
BatchUpdateException se lanza cuando ocurre un error durante una operación de actualización por lotes. Además de la información proporcionada por SQLException, BatchUpdateException proporciona las cuentas de actualización para todas las declaraciones que se ejecutaron antes de que ocurriera el error.
SQLClientInfoException se lanza cuando no se pueden establecer una o más propiedades de información del cliente en una Connection. Además de la información proporcionada por SQLException, SQLClientInfoException proporciona una lista de propiedades de información del cliente que no se establecieron.
El siguiente método, showCafes, muestra el contenido de la tabla Cafes y demuestra el uso de objetos ResultSet y cursores:
publicstaticvoidshowCafes(Connection con) throws SQLException {
String query ="select nome, idProveedor, precio, ventas, total from Cafe";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String nombreCafe = rs.getString("nome");
int idProveedor = rs.getInt("idProveedor");
float precio = rs.getFloat("precio");
int ventas = rs.getInt("ventas");
int total = rs.getInt("total");
System.out.println(nombreCafe +", "+ idProveedor +", "+ precio +", "+ ventas +", "+ total);
}
} catch (SQLException e) {
// Manejo de excepciones }
}
Un objeto ResultSet es una tabla de datos que representa un conjunto de resultados de una base de datos, usualmente mediante la ejecución de una declaración que consulta la base de datos.
Por ejemplo, el método showCafes crea un ResultSet, rs, cuando ejecuta la consulta a través del objeto Statement, stmt.
Un objeto ResultSet se puede crear a través de cualquier objeto que implemente la interfaz Statement:
Se accede a los datos en un objeto ResultSet a través de un cursor, que no es un cursor de base de datos.
Un objeto ResultSet es un puntero que apunta a una fila de datos en el ResultSet.
Inicialmente, el cursor se sitúa antes de la primera fila.
El método ResultSet.next() mueve el cursor a la siguiente fila. Este método devuelve false si el cursor está situado después de la última fila.
ResultSet.next() se llama repetidamente al método ResultSet.next() con un bucle while para iterar a través de todos los datos en el ResultSet.
Veremos a continuación:
Interfaz ResultSet
Recuperación de valores de columnas de cada fila/registro.
Cursores
Actualización de Filas en Objetos ResultSet
Uso de Objetos Statement para Actualizaciones batch
Inserción de Filas en Objetos ResultSet
1. Interfaz ResultSet
La interfaz ResultSet dispone de métodos para recuperar y manipular los resultados de consultas ejecutadas.
Pueden crearse objetos ResultSet con funcionalidades y características diferentes:
Tipo de cursor.
Concurrencia.
“Retención” del cursor.
A) Tipos de ResultSet
El primer argumento de los métodos createStatement, prepareStatement y prepareCall de Connection es el tipo de ResultSet.
El tipo de un objeto ResultSet determina el nivel de funcionalidad en dos aspectos:
Las formas en que se puede manipular el cursor (hacia adelante, hacia atrás, a una posición absoluta y así sucesivamente).
Cómo se reflejan los cambios concurrentes realizados en la fuente de datos (base de datos) mediante el objeto ResultSet: si se reflejan o no y cuándo se reflejan.
La sensibilidad de un objeto ResultSet está determinada por uno de los tres tipos diferentes de ResultSet:
TYPE_FORWARD_ONLY: el cursor se mueve solo hacia adelante, desde antes de la primera fila hasta después de la última fila. A veces se recupera fila a fila y no todos los resultados de una vez.
TYPE_SCROLL_INSENSITIVE: el cursor puede moverse hacia adelante y hacia atrás con respecto a la posición actual, y puede moverse a una posición absoluta. El ResultSet no es sensible a los cambios realizados en la base de datos mientras está abierto.
TYPE_SCROLL_SENSITIVE: el cursor puede moverse hacia adelante y hacia atrás con respecto a la posición actual, y puede moverse a una posición absoluta. El ResultSet refleja los cambios realizados en la base de datos subyacente mientras está abierto.
El tipo de ResultSet predeterminado es TYPE_FORWARD_ONLY.
Nota: No todas las bases de datos y controladores JDBC admiten todos los tipos de ResultSet.
El método DatabaseMetaData.supportsResultSetType devuelve true si el tipo de ResultSet especificado es compatible y false en caso contrario.
B) Concurrencia de ResultSet (actualizable o no)
Es el segundo argumento de la createStatement, prepareStatement o prepareCall de Connection es la concurrencia.
La concurrencia de un objeto ResultSet determina qué nivel de funcionalidad de actualización se admite.
Hay dos niveles de concurrencia:
CONCUR_READ_ONLY: ResultSet no se puede actualizar.
CONCUR_UPDATABLE: ResultSet se puede actualizar.
La concurrencia predeterminada de ResultSet es CONCUR_READ_ONLY.
Nota: No todos los controladores JDBC y bases de datos admiten la concurrencia. El método DatabaseMetaData.supportsResultSetConcurrency devuelve true si el nivel de concurrencia especificado es compatible con el controlador y false en caso contrario.
Comprobación de si un ResultSet admite determinados niveles de concurrencia, tipo y actualización:
El siguiente ejemplo muestra cómo usar un objeto ResultSet cuyo nivel de concurrencia es CONCUR_UPDATABLE:
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT a, b FROM Tabla1");
// rs será desplazable y no mostrará cambios realizados por otros.// Será actualizable.
El método actualizarPrecios demuestra cómo usar un objeto ResultSet cuyo nivel de concurrencia es CONCUR_UPDATABLE:
publicvoidactualizarPrecios(float porcetaje) throws SQLException {
try (Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
ResultSet uprs = stmt.executeQuery("SELECT * FROM Cafe");
while (uprs.next()) {
float f = uprs.getFloat("precio");
uprs.updateFloat("precio", f * porcetaje);
uprs.updateRow();
}
} catch (SQLException e) {
// Manejo de excepciones }
}
C) Retención del Cursor (permanece abiertos cuando se llama al método commit)
Llamar al método Connection.commit (confirmar la transacción) puede cerrar los objetos ResultSet que se hayan creado durante la transacción actual.
En algunos casos, esto puede no ser el deseado. La propiedad holdability del ResultSet le da a la aplicación control sobre si los objetos ResultSet (cursores) se cierran cuando se llama a commit.
Las siguientes constantes de ResultSet se pueden suministrar a los métodos createStatement, prepareStatement y prepareCall de Connection:
HOLD_CURSORS_OVER_COMMIT: los cursores ResultSet no se cierran; permanecen abiertos cuando se llama al método commit. Los cursores retenidos pueden ser ideales si la aplicación utiliza principalmente objetos ResultSet de solo lectura.
CLOSE_CURSORS_AT_COMMIT: Los objetos ResultSet (cursores) se cierran cuando se llama al método commit. Cerrar cursores al llamar a este método puede dar mejor rendimiento para algunas aplicaciones.
La retención predeterminada del cursor varía según el SGBD.
Retención predeterminada del cursor
Nota: No todos los controladores JDBC y bases de datos admiten cursores retenibles y no retenibles.
El método DatabaseMetaData.supportsResultSetHoldability devuelve true si el nivel de retención especificado es compatible con el controlador y false en caso contrario.
El método admiteRetencion muestra la retención predeterminada del cursor de los objetos ResultSet y si se admiten HOLD_CURSORS_OVER_COMMIT y CLOSE_CURSORS_AT_COMMIT:
ResultSet tiene métodos getXXX (por ejemplo, getBoolean(indice/nombre) y getLong(indice/nombre) para recuperar valores de columna desde la fila actual:
Se pueden recuperar valores utilizando el número de índice de la columna o el alias o nombre de la columna.
El índice de columna suele ser más eficiente. Las columnas se numeran a partir de 1.
Para máxima portabilidad, las columnas del conjunto de resultados dentro de cada fila deben leerse en orden de izquierda a derecha, y cada columna debe leerse solo una vez.
Por ejemplo, el siguiente método, showCafesPorIndice, recupera valores de columna por número:
publicstaticvoidshowCafesPorIndice(Connection con) throws SQLException {
String query ="select nome, idProveedor, precio, ventas, total from Cafe";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String nombreCafe = rs.getString(1);
int idProveedor = rs.getInt(2);
float precio = rs.getFloat(3);
int ventas = rs.getInt(4);
int total = rs.getInt(5);
System.out.println(nombreCafe +", "+ idProveedor +", "+ precio +", "+ ventas +", "+ total);
}
} catch (SQLException e) {
// Manejo de excepciones }
}
Los parámetros de String de todos los métodos de get no distinguen mayúsculas de minúsculas.
Una llamada a un método get con String y más de una columna tiene el mismo alias o nombre, devuelve el valor de la primera columna coincidente.
La opción de usar una cadena en lugar de un número entero está diseñada para utilizarse cuando las columnas tienen alias o nombres en la consulta SQL que generó el conjunto de resultados.
Para columnas que no se nombran explícitamente en la consulta (por ejemplo, select * from Cafe), es mejor emplear números de columnas.
getString con nombres únicos
Si se utilizan nombres de columna, se debe garantizar que se refieran de manera única a las columnas previstas mediante el uso de alias de columna, por medio de la cláusula SQL AS en la declaración SELECT.
getString con para recuperar otros tipos de datos
Nota: se recomienda el método getString para recuperar los tipos de SQL CHAR y VARCHAR, pero es posible recuperar cualquier tipo de SQL básicos con él.
Obtener todos los valores con getString puede ser muy cómodo, pero convierte el valor numérico en un objeto String de Java.
Para tipos de datos no estándar SQL3 emplea getString.
3. Moviendo el cursor
Se accede a los datos en un objeto ResultSet a través de un cursor, que apunta a una fila en el objeto ResultSet.
Cuando se crea un objeto ResultSet, el cursor se sitúa antes de la primera fila.
El método showCafes mueve el cursor llamando al método ResultSet.next(). Hay otros métodos disponibles para mover el cursor:
next: mueve el cursor hacia adelante una fila. Devuelve true si el cursor está en una fila y false si se sitúa después de la última fila.
previous: mueve el cursor hacia atrás una fila. Devuelve true si el cursor está en una fila y false si el cursor está antes de la primera fila.
first: mueve el cursor a la primera fila en el objeto ResultSet. Devuelve true si el cursor está en la primera fila y false si el objeto ResultSet no contiene ninguna fila.
last: mueve el cursor a la última fila en el objeto ResultSet. Devuelve true si el cursor está en la última fila y false si el objeto ResultSet no contiene ninguna fila.
beforeFirst: sitúa el cursor al comienzo del objeto ResultSet, antes de la primera fila. Si el objeto ResultSet no contiene ninguna fila, este método no tiene efecto.
afterLast: sitúa el cursor al final del objeto ResultSet, después de la última fila. Si el objeto ResultSet no contiene ninguna fila, este método no tiene efecto.
relative(int rows): mueve el cursor en relación con su posición actual.
absolute(int row): sitúa el cursor en la fila especificada por el parámetro row.
La sensibilidad predeterminada de un ResultSet es TYPE_FORWARD_ONLY, lo que significa que no se puede desplazar.
No se puede llamar a ninguno de estos métodos que mueven el cursor, excepto next, si el ResultSet no se puede desplazar.
4. Actualización de Filas con ResultSet
No se puede actualizar un objeto ResultSet con TYPE_FORWARD_ONLY.
Los ResultSet que pueden moverse (TYPE_SCROLL_SENSITIVE y TYPE_SCROLL_INSENSITIVE) (el cursor puede moverse hacia atrás o a una posición absoluta) pueden actualizarse.
Existen métodos de actualización de campos de ResultSet para todos los tipos de datos SQL:
updateBoolean, updateByte, updateShort, updateInt, updateLong, updateFloat, updateDouble,
updateBigDecimal, updateString, updateBytes, updateDate, updateTime, updateTimestamp, updateAsciiStream,
updateBinaryStream, updateCharacterStream, updateObject.
Estos métodos actualizan el valor de un campo en la fila actual.
Una vez actualizado el valor de un campo, se debe llamar al método updateRow para que se haga efectivo el cambio en la base de datos.
El siguiente método, actualizaPrecios, multiplica la columna precio de cada fila por el porcentaje argumentado:
publicvoidactualizaPrecios(float percentage) throws SQLException {
try (Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
ResultSet uprs = stmt.executeQuery("SELECT * FROM Cafe");
while (uprs.next()) {
float f = uprs.getFloat("precio");
uprs.updateFloat("precio", f * percentaje);
uprs.updateRow();
}
} catch (SQLException e) {
// Manejo de excepciones }
}
En el ejemplo:
El campo ResultSet.TYPE_SCROLL_SENSITIVE crea un objeto ResultSet cuyo cursor puede moverse.
El campo ResultSet.CONCUR_UPDATABLE crea un objeto ResultSet que se puede actualizar. Si no se especifica, el objeto ResultSet es de solo lectura.
El método ResultSet.updateFloat(campo, valor) actualiza la columna especificada (en este ejemplo, precio) con el valor float especificado en la fila donde está posicionado el cursor. ResultSet contiene varios métodos actualizadores que te permiten actualizar valores de columnas de varios tipos de datos. Para actualizar debe llamarse al método ResultSet.updateRow().
Los objetos Statement, PreparedStatement y CallableStatement tienen una lista de órdenes batch asociadas que
podemos añadir con el [método addBatch(String s)](https://docs.oracle.com/en/java/javase/21/docs/api/java.
sql/java/sql/Statement.html#addBatch(java.lang.String)).
No puede contener una declaración que produzca un ResultSet, como una declaración SELECT.
La lista de procesos Batch sólo puede contener declaraciones que produzcan una tarea de actualización
(UPDATE, INSERT, etc.) o de tipo DLL (CREATE TABLE, DROP TABLE, ALTER TABLE, etc.).
La lista se asocia con un objeto Statement en su creación (método addBatch) y está inicialmente vacía.
Se pueden añadir sentencias SQL a esta lista con el método addBatch y vaciarla con el método clearBatch.
Al terminar de añadir órdenes batch se invoca al método executeBatch para enviarlas todas a la base de datos
para que se ejecuten como una unidad o lote.
Por ejemplo, el siguiente método, batchUpdate, añade cuatro filas a la tabla Cafe con una actualización por
lotes (batch):
publicvoidbatchUpdate() throws SQLException {
con.setAutoCommit(false); // deshabilita el modo de autocommittry (Statement stmt = con.createStatement()) {
stmt.addBatch("INSERT INTO Cafe "+"VALUES('Amaretto', 49, 9.99, 0, 0)");
stmt.addBatch("INSERT INTO Cafe "+"VALUES('Avellana', 49, 9.99, 0, 0)");
stmt.addBatch("INSERT INTO Cafe "+"VALUES('Amaretto_decaf', 49, 10.99, 0, 0)");
stmt.addBatch("INSERT INTO Cafe "+"VALUES('Avellana_decaf', 49, 10.99, 0, 0)");
int[] updateCounts = stmt.executeBatch();
con.commit();
} catch (BatchUpdateException b) {
// } catch (SQLException ex) {
// } finally {
con.setAutoCommit(true);
}
}
La línea siguiente deshabilita el modo de autocommit para el objeto Connection con, de modo que la transacción
NO se comprometerá ni se revertirá automáticamente cuando se llame al método executeBatch.
con.setAutoCommit(false);
Para permitir un manejo de errores correcto, siempre debes deshabilitar el modo de autocommit antes de comenzar
una actualización batch.
Para enviar las órdenes SQL que se agregaron a la lista y que se ejecuten como un lote (st.executeBatch()):
int[] updateCounts = stmt.executeBatch();
stmtutiliza el método executeBatch para enviar el lote de INSERT, no el método executeUpdate, que
envía sólo una orden y devuelve un sólo recuento de actualización.
El SGBD ejecuta las órdenes en el orden en que se agregaron a la lista, por lo que primero agregará la fila de
valores para “Amaretto”, “Avellana”,…
Si los cuatro comandos se ejecutan correctamente, el método stmt.executeBatch() devuelve el recuento de
actualización para cada orden SQL en el orden en que se ejecutó. Los recuentos de actualización que indican
cuántas filas afectó cada comando se almacenan en el array updateCounts (puedes llamarle cómo quieras).
Si los cuatro comandos en el lote se ejecutan correctamente, updateCounts contendrá cuatro valores, en este caso
con 1 porque una inserción afecta a una fila. La lista de comandos asociados con stmt ahora
estará vacía porque los cuatro comandos agregados anteriormente se enviaron a la base de datos cuando stmt llamó
al método executeBatch.
Se puede vaciar explícitamente esta lista de comandos en cualquier momento con el método clearBatch.
El método Connection.commit hace que el lote de actualizaciones en la tabla Cafe sea permanente. Este método debe
llamarse de manera explícita, porque se deshabilitó el modo de autocommit previamente para esta conexión.
Volvemos a habilitar el modo autocommit para el objeto Connection:
con.setAutoCommit(true);
Es importante para que se vuelva a hacer commit de manera automática, y evitamos tener que llamar a commit.
2. Actualización batch parametrizada
También es posible realizar una actualización en lote parametrizada:
con.setAutoCommit(false);
PreparedStatement pstmt = con.prepareStatement(
"INSERT INTO Cafe VALUES( "+"?, ?, ?, ?, ?)");
pstmt.setString(1, "Amaretto");
pstmt.setInt(2, 49);
pstmt.setFloat(3, 9.99);
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.addBatch();
pstmt.setString(1, "Avellana");
pstmt.setInt(2, 49);
pstmt.setFloat(3, 9.99);
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.addBatch();
// ... y así sucesivamente para cada nuevo// tipo de caféint[] updateCounts = pstmt.executeBatch();
con.commit();
con.setAutoCommit(true);
(1) Una de las declaraciones SQL que añadida al lote produce un ResultSet (por lo general, una consulta)
(2) Una de las declaraciones SQL no se ejecuta correctamente por alguna otra razón.
Recuerda que no se debe agregar una consulta (una declaración SELECT) a un lote de comandos SQL porque el método
executeBatch, que devuelve un array de contador de actualizaciones, espera un recuento de actualización de cada
declaración SQL que se ejecute correctamente:
INSERT INTO, UPDATE, DELETE, que devuelven el número de filas afectadas.
CREATE TABLE, DROP TABLE, ALTER TABLE, que devuelven 0.
executeBatch.
Una BatchUpdateException contiene un array de recuentos de actualización similar al array devuelto por el método executeBatch.
En ambos casos, los recuentos de actualización están en el mismo orden que los comandos que los produjeron.
Esto nos sirve para saber cuántos comandos en el lote se ejecutaron correctamente y cuáles son.
Por ejemplo, si cinco comandos se ejecutaron correctamente, el array contendrá cinco números: el primero será el
recuento de actualización para el primer comando, el segundo será el recuento de actualización para el segundo
comando y así sucesivamente.
El método, printBatchUpdateException, imprime toda la información de SQLException más los recuentos de
actualización contenidos en un objeto BatchUpdateException.
Dado que BatchUpdateException.getUpdateCounts devuelve un array de int, el código usa un bucle for para imprimir cada uno de los recuentos de actualización:
publicstaticvoidprintBatchUpdateException(BatchUpdateException b) {
System.err.println("----BatchUpdateException----");
System.err.println("SQLState: "+ b.getSQLState());
System.err.println("Message: "+ b.getMessage());
System.err.println("Vendor: "+ b.getErrorCode());
System.err.print("Update counts: ");
int[] updateCounts = b.getUpdateCounts(); // array de int con los recuentos de actualizaciónfor (int i = 0; i < updateCounts.length; i++) {
System.err.print(updateCounts[i]+" ");
}
}
Batch vs transacción
SQL Batch:
a) SQL Batch es una colección de sentencias que deben ejecutarse sin garantía de éxito o fracaso.
b) El procesamiento por lotes significa que las cosas se sitúan en una cola y se procesan cuando se alcanza cierta cantidad de elementos o cuando ha transcurrido cierto período de tiempo. Se puede deshacer/retroceder en esto.
SQL Transaction:
a) La Transacción SQL es una colección de sentencias que están garantizadas para tener éxito o fallar totalmente.
Las transacciones no completarán la mitad de los comandos y luego fallarán en el resto; si uno falla, todos fallan.
b) La transacción es como un procesamiento en tiempo real que te permite deshacer/retroceder cambios.
En las TRANSACCIONES, es similar al lote, pero tienes la opción de “cancelarla”.
Por ejemplo, si el banco procesa tu solicitud de depósito y luego descubre que no tienes suficiente dinero en
tu cuenta para cubrir el depósito, el banco puede cancelar la transacción y devolverte el cheque.
El banco no puede hacer esto con el procesamiento por lotes.
La interface PreparedStatement hereda de Statement y representa una sentencia SQL precompilada.
1. Características de PreparedStatement
En la mayoría de los casos se recomienda el uso de PreparedStatement para enviar sentencias SQL a la base de datos.
Una vez compilada, la sentencia preparada se puede ejecutar varias veces.
PreparedStatement es más eficientes que las sentencias Statement cuando se ejecutan varias veces, ya que la sentencia SQL se analiza y se compila solo una vez.
PreparedStatement también son útiles cuando se ejecutan consultas dinámicas, ya que permiten la separación de la sentencia SQL de los parámetros.
En cuanto al uso, la diferencia principal de un objeto PreparedStatement es que, a diferencia de un objeto Statement, se le proporciona una declaración SQL cuando se crea.
En la mayoría de los casos esta declaración SQL se envía al SGBD de inmediato, donde se compila.
Como resultado, el objeto PreparedStatement contiene una declaración SQL que ha sido precompilada. Cuando se ejecuta el PreparedStatement, el SGB puede ejecutar la declaración SQL del PreparedStatement sin tener que compilarla primero.
Es la opción idónea para declaraciones SQL que toman parámetros, pues se puede usar la misma declaración y suministrar diferentes valores cada vez que se ejecuta.
La principal ventaja es que evita la inyección SQL, pues los parámetros se pasan por separado de la consulta SQL.
Inyeccion SQL
La inyección SQL es una técnica para explotar maliciosamente aplicaciones que utilizan datos proporcionados por el
cliente en declaraciones SQL. Los atacantes engañan al motor SQL para ejecutar comandos no deseados al suministrar
una entrada de cadena especialmente diseñada, obteniendo así acceso no autorizado a una base de datos para ver o
manipular datos restringidos:
Las sentencias preparadas siempre tratan los datos proporcionados por el cliente como contenido de un parámetro y
nunca como parte de una declaración SQL.
Ejemplo:
publicvoidupdateVentas(HashMap<String, Integer> ventasPorSemana) throws SQLException {
String updateString ="update Cafe set ventas = ? where nome = ?"; // Actualización de ventas. String updateStatement ="update Cafe set total = total + ? where nome = ?"; // Actualización del total.try (PreparedStatement updateVentas = con.prepareStatement(updateString);
PreparedStatement updateTotal = con.prepareStatement(updateStatement)) {
con.setAutoCommit(false);
for (Map.Entry<String, Integer> e : ventasPorSemana.entrySet()) {
updateVentas.setInt(1, e.getValue().intValue());
updateVentas.setString(2, e.getKey());
updateVentas.executeUpdate(); // Actualización de ventas. updateTotal.setInt(1, e.getValue().intValue());
updateTotal.setString(2, e.getKey());
updateTotal.executeUpdate(); // Incremento del total. con.commit();
}
} catch (SQLException e) {
// Gestión de excepciones.if (con !=null) {
try {
System.err.print("La transacción se está revirtiendo");
con.rollback();
} catch (SQLException excep) {
// Gestión de excepciones. }
}
}
}
Después de darle un valor a un parámetro se retiene ese valor hasta que se restablece a otro valor o se llama al método clearParameters.
// cambia la columna ventas de Buñuelos//fila a 100updateVentas.setInt(1, 100); // Si no se cambia el valor, se mantendrá en 100.updateVentas.setString(2, "Buñuelos");
updateVentas.executeUpdate();
// cambia la columna ventas de Tortitas americanas a 100// (el primer parámetro se quedó en 100, y el segundo// parámetro se restableció a "Tortitas americanas")updateVentas.setString(2, "Tortitas americanas");
updateVentas.executeUpdate();
Uso de bucles para asignar valores:
Se puede facilitar la codificación mediante el uso de un bucle para asignar valores para los parámetros de entrada.
El método updateVentas utiliza un bucle for-each para establecer repetidamente valores en los objetos PreparedStatement updateVentas y updateTotal:
for (Map.Entry<String, Integer> e : ventasPorSemana.entrySet()) {
updateVentas.setInt(1, e.getValue().intValue());
updateVentas.setString(2, e.getKey());
// ...}
El método updateVentas toma un argumento, HashMap.
Cada elemento en el argumento HashMap contiene el nombre y la cantidad vendida durante la semana actual.
El bucle for-each itera a través de cada elemento del HashMap.
3. Ejecución de sentencias con PreparedStatement: executeUpdate, executeQuery y execute.
Al igual que con los objetos Statement, para ejecutar un objeto PreparedStatement pude invocar:
executeQuery si la consulta devuelve solo un ResultSet (como una declaración SQL SELECT).
executeUpdate si la consulta no devuelve un ResultSet (como una declaración SQL UPDATE o INSERT).
execute si la consulta podría devolver más de un objeto ResultSet.
En updateVentas(HashMap<String, Integer>) son sentencias UPDATE, por lo que usa executeUpdate:
Nota: Al principio de updateVentas, el modo de confirmación automática se establece en false:
con.setAutoCommit(false);
En consecuencia, ninguna declaración SQL se confirma hasta que se llama al método commit.
Más adelante veremos cómo realizar transacciones.
4. Valores devueltos por executeUpdate
El valor de devuelto para executeUpdate es un valor int que indica cuántas filas de una tabla se actualizaron.
Por ejemplo:
updateVentas.setInt(1, 50);
updateVentas.setString(2, "Tortitas americanas");
int n = updateVentas.executeUpdate();
// n = 1 porque se cambió una fila.
Esa actualización afecta a una fila en la tabla, por lo que n es igual a 1.
Cuando el método executeUpdate se utiliza para ejecutaruna declaración DDL (lenguaje de definición de datos),
como en la creación de una tabla, devuelve el valor int de 0.
Por ejemplo:
// n = 0int n = executeUpdate(crearTablaCafe); // Devuelve º filas afectadas.
Cuando el valor de devuelto por executeUpdate es 0, puede significar:
La declaración ejecutada fue una declaración de actualización que no afectó a ninguna fila.
En muchos casos de uso, es posible que desee ejecutar varias declaraciones SQL como una unidad de trabajo. Por ejemplo,
supongamos que tiene una aplicación que actualiza los datos de una tabla y luego actualiza los datos de otra tabla.
Desea asegurarse de que ambas actualizaciones se realicen correctamente o que no se realice ninguna de ellas.
La ejecución de varias declaraciones SQL como una unidad de trabajo se denomina transacción.
1. Desactivación de Auto-Commit
Por defecto, una conexión JDBC está en modo de auto-commit.
Cada sentencia SQL se trata como una transacción y se confirma automáticamente justo después de ejecutarse.
Para permitir que dos o más sentencias se agrupen en una transacción se debe desactivar el modo de auto-commit.
Ninguna sentencia SQL se confirma hasta que se llame explícitamente al método commit.
Todas las sentencias ejecutadas después de la llamada al método commit se incluyen en la transacción actual y se confirman juntas como una unidad.
(Para ser más preciso, el valor predeterminado es que una declaración SQL se confirme cuando se completa, no cuando
se ejecuta. Sin embargo, en casi todos los casos, una declaración se completa y, por lo tanto, se confirma, justo después de ejecutarse.)
Desactivación de de auto-commit:
con.setAutoCommit(false);
2. Commit de transacciones
Ninguna declaración SQL se confirma hasta que se llame al método commit:
publicvoidupdateVentas(HashMap<String, Integer> ventasPorSemana) throws SQLException {
try (PreparedStatement psVentas = con.prepareStatement("update Producto set ventas = ? where nome = ?");
PreparedStatement psTotal = con.prepareStatement("update Producto set total = total + ? where nome = ?")) {
con.setAutoCommit(false); // Deshabilita el modo de autocommitfor (Map.Entry<String, Integer> e : ventasPorSemana.entrySet()) {
psVentas.setInt(1, e.getValue().intValue());
psVentas.setString(2, e.getKey());
psVentas.executeUpdate();
psTotal.setInt(1, e.getValue().intValue());
psTotal.setString(2, e.getKey());
psTotal.executeUpdate();
con.commit(); // Confirmación }
} catch (SQLException e) {
// Gestión de excepciones.if (con !=null) {
try {
System.err.print("La transacción se está revirtiendo");
con.rollback(); // Revierte la transacción } catch (SQLException excep) {
// Gestión de excepciones. }
}
}
}
Las dos declaraciones preparadas psVentas y psTotal se confirman juntas cuando se llama al método commit().
Cada vez que se llama al método commit (ya sea automáticamente cuando se habilita el modo de auto-commit o explícitamente cuando se deshabilita), todos los cambios se vuelven permanentes.
La declaración con.setAutoCommit(true); habilita el modo de auto-commit, cada declaración se confirma automáticamente
cuando se completa.
Desactivación y activación de auto-commit
Es recomendable desactivar el modo de auto-commit únicamente durante el modo de transacción.
De esta manera, se evita mantener bloqueos de base de datos para múltiples declaraciones, lo que aumenta la probabilidad de conflictos con otros usuarios.
3. Puntos de Guardado
El método Connection.setSavepoint establece un objeto Savepoint (punto de guardado) dentro de la transacción actual.
El método Connection.rollback se sobrecarga para aceptar un argumento Savepoint, un punto de guardado dentro de la transacción actual.
El siguiente método, Producto.modificarPreciosPorPoncertaje, aumenta el precio de un café en particular por un porcentaje,
porcentaje. Sin embargo, si el nuevo precio es mayor que un precio especificado, precioMaximo, entonces el precio se revierte al precio original:
publicvoidmodificarPreciosPorPoncertaje(String nombre, float porcentaje, float precioMaximo)
throws SQLException {
con.setAutoCommit(false);
ResultSet rs =null;
String precioQuery ="SELECT nombre, precio FROM Producto WHERE nombre = ?";
String updateQuery ="UPDATE Producto SET precio = ? WHERE nombre = ?";
try (PreparedStatement psPrecio = con.prepareStatement(precioQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
PreparedStatement updatePrice = con.prepareStatement(updateQuery)) {
Savepoint puntoSalvar = con.setSavepoint(); // Creación de punto de guardado psPrecio.setString(1, nombre);
if (!psPrecio.execute()) { // Si no hay resultados System.out.println("No puedo encontrar el producto con nombre: "+ nombre);
} else {
rs = psPrecio.getResultSet();
rs.first(); // sitúa el cursor en la primera filafloat precioAnterior = rs.getFloat("precio");
float precioNuevo = precioAnterior + (precioAnterior * porcentaje);
System.out.printf("Precio anterio de %s es $%.2f%n", nombre, precioAnterior);
System.out.printf("Nuevo precio de %s es $%.2f%n", nombre, precioNuevo);
System.out.println("Realizando actualización...");
updatePrice.setFloat(1, precioNuevo);
updatePrice.setString(2, nombre);
updatePrice.executeUpdate();
System.out.println("\nProducto después de actualización:");
Producto.verTabla(con); // Ver tabla (debe implantarse)if (precioNuevo > precioMaximo) { // Si supera el máximo se hace un rollback al punto de guardado System.out.printf("El nuevo precio, $%.2f, es mayor que el precio máximo, $%.2f. "+"Revertiendo la transacción...%n", precioNuevo, precioMaximo);
con.rollback(puntoSalvar);
System.out.println("\nProducto después de revertir:");
Producto.viewTable(con); // Ver tabla (debe implantarse) }
con.commit(); // Commit de la transacción }
} catch (SQLException e) {
// Gestión de excepciones. } finally {
con.setAutoCommit(true);
}
}
Se especifica que el cursor ResultSet generado por psPrecio se cierra cuando se llama al método commit.
Ten en cuenta que si el SGBD no admite ResultSet.CLOSE_CURSORS_AT_COMMIT, ésta se ignora:
El método comienza creando un Savepoint con la siguiente instrucción:
Savepoint puntoSalvar = con.setSavepoint();
El método verifica si el nuevo precio es mayor que el valor de precioMaximo. Si es así, el método deshace la transacción hasta el punto de guardado con la siguiente instrucción:
con.rollback(puntoSalvar);
Cuando el método realiza la transacción llamando al método Connection.commit, no comprometerá ninguna fila cuyo Savepoint
asociado haya sido deshecho; comprometerá todas las demás filas actualizadas.
4. Liberación de Puntos de Guardado
El método Connection.releaseSavepoint toma un objeto Savepoint como parámetro y lo elimina de la transacción actual.
Después de que se ha liberado un punto de guardado, intentar hacer referencia en una operación de deshacer provoca que
se lance una SQLException.
Cualquier punto de guardado que se haya creado en una transacción se libera automáticamente y se vuelve inválido cuando
la transacción se confirma o cuando se revierte por completo la transacción.
Deshacer una transacción hasta un punto de guardado libera automáticamente y vuelve inválidos cualquier otro punto de
guardado que se haya creado después del punto de guardado en cuestión.
5. Método rollback:
Llamar al método rollback termina una transacción y devuelve los valores que se modificaron a sus valores anteriores.
Si se intenta ejecutar una o más declaraciones en una transacción y se obtiene un SQLException, debe invocarse al método rollback para finalizar la transacción y comenzarla de nuevo.
Capturar un SQLException indica que hay errores, pero no te dice qué se ha comprometido o no.
Debido a que no no se puede saber qsi se ha completado alguna sentencia, llamar al método rollback es la única forma de estar seguro.
El método Producto.updateVentas demuestra una transacción e incluye un bloque catch que invoca al método rollback.
Si la aplicación continúa y utiliza los resultados de la transacción, esta llamada al método rollback
en el bloque catch evita el uso de datos posiblemente incorrectos.
6. Utilizando Transacciones en la integridad de los datos
Las transacciones pueden ayudar a preservar la integridad de los datos en una tabla.
El uso de transacciones proporciona algún nivel de protección contra conflictos que surgen cuando dos usuarios acceden a datos al mismo tiempo.
Para evitar conflictos durante una transacción, un SGBD utiliza bloqueos:
Mecanismo para bloquear el acceso de otros a los datos que está siendo accedido por la transacción.
(Ten en cuenta que en el modo de autocommit, donde cada declaración es una transacción, los bloqueos se mantienen solo para una declaración).
Un bloqueo permanece vigente hasta que la transacción se confirma o se revierte.
Bloqueos
Los bloqueos pueden causar problemas de rendimiento.
Por ejemplo, un DBMS podría bloquear una fila de una tabla hasta que las actualizaciones en ella se hayan confirmado.
El efecto de este bloqueo sería evitar que un usuario obtenga una lectura sucia, es decir, leer un valor antes de que
se haga permanente. (Acceder a un valor actualizado que no se ha confirmado se considera una lectura sucia porque es
posible que ese valor se revierta a su valor anterior. Si lees un valor que luego se revierte, habrás leído un valor no
válido.)
Nivel de aislamiento de transacción
Cómo se establecen los bloqueos está determinado por lo que se llama un nivel de aislamiento de transacción,
que puede variar desde no admitir transacciones en absoluto hasta admitir transacciones que imponen reglas de acceso muy estrictas.
Un ejemplo de un nivel de aislamiento de transacción es TRANSACTION_READ_COMMITTED, que no permitirá que se acceda a un
valor hasta después de que se haya confirmado.
En otras palabras, si el nivel de aislamiento de la transacción se establece en TRANSACTION_READ_COMMITTED, el DBMS no
permite lecturas sucias.
La interfaz Connection incluye cinco valores que representan los niveles de aislamiento de transacción:
Nivel de Aislamiento
Transacciones
Lecturas Sucias
Lecturas No Repetibles
Lecturas Fantasmales
TRANSACTION_NONE
No admitido
No aplicable
No aplicable
No aplicable
TRANSACTION_READ_COMMITTED
Admitido
Prevenido
Permitido
Permitido
TRANSACTION_READ_UNCOMMITTED
Admitido
Permitido
Permitido
Permitido
TRANSACTION_REPEATABLE_READ
Admitido
Prevenido
Prevenido
Permitido
TRANSACTION_SERIALIZABLE
Admitido
Prevenido
Prevenido
Prevenido
Una lectura no repetible ocurre cuando la transacción A recupera una fila, la transacción B actualiza posteriormente la fila,
y la transacción A vuelve a recuperar la misma fila. La transacción A recupera la misma fila dos veces pero ve datos diferentes.
Una lectura fantasma ocurre cuando la transacción A recupera un conjunto de filas que cumplen con una condición dada,
la transacción B inserta o actualiza posteriormente una fila de manera que ahora cumple con la condición en la transacción A,
y la transacción A repite más tarde la recuperación condicional. La transacción A ahora ve una fila adicional.
A esta fila se le denomina fantasma.
No necesitas hacer nada respecto al nivel de aislamiento de la transacción; puedes usar el predeterminado para el SGBD empleado.
El nivel de aislamiento de transacción predeterminado depende del SGBD.
Por ejemplo, para Java DB, es TRANSACTION_READ_COMMITTED. JDBC te permite averiguar a qué nivel de aislamiento de
transacción está configurado tu DBMS (usando el método getTransactionIsolation de Connection) y también te permite
establecerlo en otro nivel (usando el método setTransactionIsolation de Connection).
Niveles de aislamiento de transacción
Nota: es muy probable que un controlador JDBC no admita todos los niveles de aislamiento de transacción.
Si un controlador no admite el nivel de aislamiento especificado en una invocación de setTransactionIsolation,
el controlador puede sustituir un nivel de aislamiento de transacción más alto y restrictivo.
Si un controlador no puede sustituir un nivel de transacción más alto, se produce una SQLException.
Utiliza el método DatabaseMetaData.supportsTransactionIsolationLevel para determinar si el controlador admite o no un nivel dado.
Muchas veces necesitamos obtener el valor de una clave primaria generada automáticamente después de insertar un registro en la base de datos.
Mediante JDBC podemos obtener el valor de la clave primaria generada automáticamente después de insertar un registro en la base de datos.
2. Configuración y creación de la tabla
A m odo de ejemplo, poder ejecutar consultas SQL, utilizaremos una base de datos H2 en memoria:
Que se conecta a la base de datos en memoria “dbConClaves” y crea una tabla llamada “Persona”.
3. Statement.RETURN_GENERATED_KEYS y getGeneratedKeys()
Una forma de obtener las claves después de la generación automática es pasar Statement.RETURN_GENERATED_KEYS al método prepareStatement():
String QUERY ="insert into Persona (nome) values (?)";
try (PreparedStatement statement = conexion.prepareStatement(QUERY, Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, "Otto");
int filasInsertadas = statement.executeUpdate();
if (filasInsertadas > 0) {
// ... } else {
// ... }
// ...} catch (SQLException e) {
// manejar la excepción relacionada con la base de datos de manera apropiada}
Después de preparar y ejecutar la consulta, se puede llamar al método getGeneratedKeys() en PreparedStatement para obtener el id:
Que llama al método next() para mover el cursor del resultado con las claves generadas.
El método getLong() obtiene obtener la primera columna como long.
Además, también es posible utilizar la misma técnica con Statements normales:
Los objetos grandes (LOB, Large Objects) son objetos de datos que pueden tener un tamaño variable y que se almacenan en
una base de datos. Se utilizan para almacenar datos como imágenes, sonidos, videos y documentos de
texto.
Por lo general, las bases de datos almacenan los datos de la siguiente forma: las columnas se agrupan en filas que,
a su vez, se apilan en bloques de datos. La información en cada bloque de datos está asociada a una fila
y los bloques de datos consumen así menos espacio en la base de datos.
Las bases de datos tratan de otro modo los objetos de datos de mayor tamaño. Los LOB superan en tamaño a las entradas convencionales de las bases de datos y no se encuentran estructurados.
En la mayoría de los casos, se almacenan en un lugar distinto.
La base de datos sólo crea en la posición que corresponda una referencia a la ubicación de almacenamiento.
Existen dos tipos de LOB:
BLOB: un BLOB es un tipo de dato que almacena un elemento grande de datos en código binario.
CLOB: un CLOB (Character Large Objects) almacena cadenas largas de caracteres. Es un término acuñado por los desarrolladores de la base de datos de Oracle.
Nota: otros sistemas de gestión de bases de datos utilizan también otros términos para denominar los objetos grandes:
MySQL/MariaDB y PostgreSQL los denominan TEXT.
Tipos de datos para almacenar objetos LOB (Large Objects) tanto binarios como de texto en diferentes Sistemas de
Gestión de Bases de Datos (SGBDR), junto con sus tamaños típicos:
MariaDB:
Binario (BLOB):BLOB o LONGBLOB (hasta 4 GB).
Texto (CLOB):TEXT o LONGTEXT (hasta 4 GB).
H2:
Binario (BLOB):BLOB (64TB)
Texto (CLOB):CLOB (64TB).
SQLite:
Binario (BLOB): No hay un tipo específico para BLOB, se pueden usar tipos de datos TEXT o BLOB (hasta 2 GB).
Texto (CLOB): No hay un tipo específico para CLOB, se pueden usar tipos de datos TEXT o BLOB (hasta 2 GB).
PostgreSQL:
Binario (BLOB):BYTEA o OID (hasta 1 GB).
Texto (CLOB):TEXT o VARCHAR (sin límite declarado, prácticamente limitado por el tamaño de la tabla).
Oracle:
Binario (BLOB):BLOB (hasta 128 TB).
Texto (CLOB):CLOB (hasta 128 TB).
MS SQL Server:
Binario (BLOB):VARBINARY(MAX) o IMAGE (hasta 2 GB en VARBINARY(MAX) y hasta 4 GB en IMAGE).
Texto (CLOB):VARCHAR(MAX) o TEXT (hasta 2 GB en VARCHAR(MAX) y hasta 2 GB en TEXT).
MariaDB y MySQL tienen cuatro tipos de datos de texto y LOB:
TINYTEXT: un texto de longitud máxima de 255 caracteres.
TEXT: un texto de longitud máxima de 65.535 caracteres (64KB).
MEDIUMTEXT: un texto de longitud máxima de 16.777.215 caracteres.
LONGTEXT: un texto de longitud máxima de 4.294.967.295 caracteres (4GB).
BLOB: un BLOB es un tipo de dato que almacena un elemento grande de datos en código binario (6GB).
MEDIUMBLOB: un BLOB de longitud máxima de 16.777.215 bytes.
LONGBLOB: un BLOB de longitud máxima de 4.294.967.295 bytes. (4GB).
TINYBLOB: un BLOB de longitud máxima de 255 bytes.
H2 tiene dos tipos de datos de texto:
CLOB/CHARACTER LARGE OBJECT: un texto de longitud máxima de 2GB.
BLOB/BINARY LARGE OBJECT: un BLOB es un tipo de dato que almacena un elemento grande de datos en código binario.
BINARY: un BLOB de longitud máxima de 2GB.
VARBINARY: un BLOB de longitud máxima de 2GB.
LONGVARBINARY: un BLOB de longitud máxima de 2GB.
1. Uso de LOB (Objetos de Gran Tamaño)
Los objetos grandes Java, como Blob, Clob y NClob pueden gestionarse desde Java sin tener que traer los datos
del servidor de la base de datos al cliente.
Muchas implementaciones representan una instancia de estos tipos de datos con un localizador (puntero) al objeto en la
base de datos.
Debido a que un objeto BLOB, CLOB o NCLOB puede ser muy grande, el uso de punteros mejora el rendimiento.
Sin embargo, algunas implementaciones gestionan (y cargan) completamente objetos grandes en cliente.
Para traer un BLOB, CLOB o NCLOB de SQL al programa cliente, se emplean métodos en las interfaces de Java
Blob,
Clob y
NClob.
1. Añadir un CLOB la Base de Datos
La interface PreparedStatement tiene métodos para asignar valores a las ? de la sentencia SQL para cada tipo de dato.
Para CLob se utiliza el método setClob:
//Insertar valoresPreparedStatement pstmt = con.prepareStatement("INSERT INTO Table(nombre, descripcion) VALUES (?, ?)");
pstmt.setString(1, "nombre de ejemplo");
pstmt.setClob(2, Files.newBufferedReader(Paths.get("E:\\descripcion.txt")));
pstmt.executeUpdate();
Clob:
El siguiente extracto de addDescripcionProducto agrega un valor SQL CLOB a la tabla Producto.
El objeto Java Clob clobDescripcion contiene el contenido del archivo especificado por nomeArquivo.
publicvoidaddDescripcionProducto(String nome, String nomeArquivo) throws SQLException {
// Cfreación del objeto Clob: Clob clobDescripcion =this.con.createClob();
try (PreparedStatement pstmt =this.con.prepareStatement("INSERT INTO Producto VALUES(?,?)");
Writer clobWriter = clobDescripcion.setCharacterStream(1);){
// setCharacterStream devuelve un objeto Writer y recibe un entero que indica la posición inicial del Clob. String str =this.readFile(nomeArquivo, clobWriter); // Lee el conteido del archivo. System.out.println("Escribo el texto: "+ clobWriter.toString());
// Si el archivo es demasiado grande, se puede escribir en el Clob en trozos. clobDescripcion.setString(1, str);
System.out.println("Longitud del clob: "+ clobDescripcion.length());
pstmt.setString(1, nome);
pstmt.setClob(2, clobDescripcion); // Se añade el Clob al PreparedStatement. pstmt.executeUpdate();
} catch (SQLException sqlex) {
// Gestión de excepciones. } catch (Exception ex) {
System.out.println("Excepción no esperada: "+ ex.toString());
}
}
private String readFile(String nomeArquivo, Writer writer) throws IOException {
try (BufferedReader br =new BufferedReader(new FileReader(nomeArquivo))) {
String nextLine ="";
StringBuffer sb =new StringBuffer();
while ((nextLine = br.readLine()) !=null) {
System.out.println("Escribiendo: "+ nextLine);
writer.write(nextLine);
sb.append(nextLine);
}
// Convertir el contenido en una cadena String datosClob = sb.toString();
// devolución de los datos.return datosClob;
}
}
a) Creación de un objeto Clob:
Clob clobDescripcion =this.con.createClob();
b) Recuperación del flujo (en este caso, un objeto Writer llamado clobWriter) que se utiliza para escribir un flujo
de caracteres en el objeto Java Clob clobDescripcion.
El método readFile escribe este flujo de caracteres; el flujo proviene del archivo especificado por la cadena
nomeArquivo. El argumento del método 1 indica que el objeto Writer comenzará a escribir el flujo de caracteres al
principio del valor Clob:
El método getDescripcion recupera el valor SQL CLOB almacenado en la columna descripcion de Producto de la fila
cuyo valor de columna nome es igual al valor de la cadena especificada por el parámetro nome:
public String getDescripcion(String nome, int numeroCaracteres) throws SQLException {
String descripcion =null;
Clob clobDescripcion =null;
String sql ="select descripcion from Producto where nome = ?";
try (PreparedStatement pstmt =this.con.prepareStatement(sql)) {
pstmt.setString(1, nome);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
clobDescripcion = rs.getClob(1);
System.out.println("Lonxitude do Clob: "+ clobDescripcion.length());
}
descripcion = clobDescripcion.getSubString(1, numeroCaracteres);
} catch (SQLException sqlex) {
// Tratamiento de excepciones. } catch (Exception ex) {
System.out.println("Excepción: "+ ex.toString());
}
return descripcion;
}
Recupera el valor Java Clob del objeto ResultSet rs:
clobDescripcion = rs.getClob(1);
Recuperación de una subcadena del objeto clobDescripcion.
La subcadena comienza en el primer carácter del valor de clobDescripcion y tiene hasta el número de caracteres consecutivos especificados en numeroCaracteres, donde numeroCaracteres es un entero.
Vamos a verlo con el método setBlob que recoge un objeto Blob.
Agregar y recuperar objetos SQL BLOB es similar a agregar y recuperar objetos CLOB.
Se precisa crear un blob, para ello se utiliza el método
createBlob.
El siguiente extracto de addImagenProducto agrega un valor SQL BLOB a la tabla Producto.
El objeto Java Blob blobImagen contiene el contenido del archivo especificado por nomeArquivo.
publicvoidaddImagenProducto(String nome, String nomeArquivo) throws SQLException {
// Creación del objeto Blob: Blob blobImagen =this.con.createBlob();
try (PreparedStatement pstmt =this.con.prepareStatement("INSERT INTO Producto VALUES(?,?)");
OutputStream blobOutputStream = blobImagen.setBinaryStream(1);){
// setBinaryStream devuelve un objeto OutputStream y recibe un entero que indica la posición inicial del Blob.byte[] bytes =this.readFile(nomeArquivo, blobOutputStream); // Lee el conteido del archivo. System.out.println("Escribo el texto: "+ blobOutputStream.toString());
// Si el archivo es demasiado grande, se puede escribir en el Blob en trozos. blobImagen.setBytes(1, bytes);
System.out.println("Longitud del blob: "+ blobImagen.length());
pstmt.setString(1, nome);
pstmt.setBlob(2, blobImagen); // Se añade el Blob al PreparedStatement. pstmt.executeUpdate();
} catch (SQLException sqlex) {
// Gestión de excepciones. } catch (Exception ex) {
System.out.println("Excepción no esperada: "+ ex.toString());
}
}
setBinaryStream de PreparedStatement
Otro modo de realizarlo es por medio del método setBinaryStream:
Este método esta sobrecargado y se puede utilizar de varias formas:
publicvoidsetBinaryStream(int index, InputStream is)
publicvoidsetBinaryStream(int index, InputStream is, int length)
publicvoidsetBinaryStream(int index, InputStream is, long length)
Por ejemplo:
//Insertar valoresPreparedStatement pstmt = con.prepareStatement("INSERT INTO Table(nombre, imagen) VALUES (?, ?)");
pstmt.setString(1, "imagen de ejemplo");
FileInputStream fin =new FileInputStream("E:\imagenes\otto.jpg");
pstmt.setBinaryStream(2, fin);
pstmt.execute();
setBytes de PreparedStatement
Otra forma de realizarlo es por medio del método setBytes:
//Insertar valoresPreparedStatement pstmt = con.prepareStatement("INSERT INTO Table(nombre, imagen) VALUES (?, ?)");
pstmt.setString(1, "imagen de ejemplo");
pstmt.setBytes(2, Files.readAllBytes(Paths.get("E:\imagenes\otto.jpg")));
pstmt.execute();
4. Liberando Recursos Retenidos por Objetos Grandes
Los objetos Java Blob, Clob y NClob siguen siendo válidos durante al menos la duración de la transacción en la que se
crearon. Esto podría resultar en que una aplicación se quede sin recursos durante una transacción de larga duración.
Las aplicaciones pueden liberar los recursos de Blob, Clob y NClob invocando su método free:
Clob aClob = con.createClob();
int numWritten = aClob.setString(1, val);
aClob.free();
Un objeto JDBC RowSet hereda de la interface ResultSet, almacenando datos en forma de tabla de una manera que lo hace más flexible y fácil de usar que un ResultSet.
El API ha definido 5 interfaces RowSet para algunos de los usos más comunes de un RowSet con implementaciones
de referencia estándar disponibles para estas interfaces RowSet. Las implementaciones de referencia estándar son JdbcRowSet,
CachedRowSet, , WebRowSet, JoinRowSet y FilteredRowSet.
JdbcRowSet es un objeto RowSet que proporciona conectividad de base de datos a través de una conexión JDBC.
CachedRowSet es un objeto RowSet que almacena datos en caché en la memoria del cliente.
WebRowSet es un objeto RowSet que proporciona conectividad de base de datos a través de una conexión JDBC y
puede escribirse en un flujo de datos XML.
JoinRowSet es un objeto RowSet que proporciona conectividad de base de datos a través de una conexión JDBC y
puede unirse a datos de varias fuentes de datos.
FilteredRowSet es un objeto RowSet que proporciona conectividad de base de datos a través de una conexión JDBC y
puede filtrar filas.
Es posible escribir las implementaciones personalizadas de la interface javax.sql.RowSet,
o heredar de las implementaciones de las cinco interfaces RowSet.
Para la mayoría de los casos las implementaciones de referencia estándar cubren todas las necesidades.
1. La interface RowSet
RowSet es una interfaz en Java que se encuentra en el módulo java.sql (no confundir RowSet con ResultSet).
RowSet está en paquete javax.sql, mientras que ResultSet está en java.sql.
La instancia de RowSet es el componente JavaBean porque tiene propiedades y un mecanismo de notificación de JavaBean.
Se introdujo en JDK5. Un JDBC RowSetproporciona una forma de almacenar los datos en forma tabular.
Hace que los datos sean más flexibles y más fáciles que un ResultSet.
La conexión entre el objeto RowSet y la fuente de datos se mantiene durante todo su ciclo de vida.
COmo hemos comentado, RowSets se clasifican en cinco categorías según cómo estén implementados, que se enumeran a continuación:
JdbcRowSet
CachedRowSet
WebRowSet
FilteredRowSet
JoinRowSet
2. Ventajas de RowSet
Es fácil y flexible de usar.
Por defecto, es desplazable y puede actualizarse, mientras que ResultSet, por defecto, solo es válido para operaciones
hacia adelante y de solo lectura.
Es un componente JavaBean (tiene propiedades y eventos), por lo que es fácil de usar en cualquier entorno y herramienta de desarrollo, además de
ser compatible con la notificación de JavaBean:
Las propiedades de un objeto RowSet se pueden establecer y obtener mediante los métodos set y get y se ven en lo IDEs.
Los eventos de un objeto RowSet se pueden registrar y recibir mediante los métodos add y remove y se ven en lo IDEs:
Los eventos se producen cuando se mueve el cursor, se actualiza una fila, se elimina una fila, se inserta una fila, etc.
RowSetEvent se genera cuando se produce un cambio en el objeto RowSet.
RowSetMetaDataEvent se genera cuando se produce un cambio en el objeto RowSetMetaData.
RowSetWarningEvent se genera cuando se produce un cambio en el objeto RowSetWarning.
Puede ser serializado.
La interfaz JDBC RowSet hereda de RowSet. Es un contenedor para el objeto ResultSet que añade características.
Métodos comunes a todas las implementaciones de RowSet:
setUrl(String url): establece la URL de la base de datos a la que se conectará el objeto RowSet.
setPassword(String password): establece la contraseña que se utilizará para conectarse a la base de datos.
setCommand(String cmd): establece el comando SQL que se utilizará para obtener los datos de la base de datos.
setInt(int parameterIndex, int x): establece el valor de un parámetro de tipo int en la consulta SQL que se utilizará para obtener los datos de la base de datos.
setString(int parameterIndex, String x): establece el valor de un parámetro de tipo String en la consulta SQL que se utilizará para obtener los datos de la base de datos.
JdbcRowSet rowSet = RowSetProvider.newFactory().createJdbcRowSet();
// 1. Base de datos OraclerowSet.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
// 2. El nombre de usuario se establece personalmente como - rootrowSet.setUsername("root");
// 3. La contraseña se establece personalmente como - passrowSet.setPassword("pass");
// 4. ConsultarowSet.setCommand("select * from Students");
Implementación
Supongamos que tenemos una tabla llamada Estudiante en la base de datos con datos:
idEstudiante
nombre
nota
1
otto
92
2
xoel
90
3
marco
80
4
xoan
82
Implementación de JdbcRowSet y recuperación de los registros:
// Programa Java para ilustrar RowSet en JDBC// Importación de base de datosimport java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.sql.RowSetEvent;
import javax.sql.RowSetListener;
import javax.sql.rowset.JdbcRowSet;
import javax.sql.rowset.RowSetProvider;
// Clase principalclassRowSetDemo {
// Método principalpublicstaticvoidmain(String args[]) {
// Bloque Try para verificar excepcionestry {
// Carga y registro de controladores Class.forName("oracle.jdbc.driver.OracleDriver");
// Creación de un RowSet JdbcRowSet rowSet = RowSetProvider.newFactory().createJdbcRowSet();
// Configuración de URL, nombre de usuario, contraseña rowSet.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
rowSet.setUsername("root");
rowSet.setPassword("pass");
// Creación de una consulta rowSet.setCommand("select * from Estudiante");
// Ejecución de la consulta rowSet.execute();
// Procesamiento de los resultadoswhile (rowSet.next()) {
// Comandos de impresión y visualización System.out.println("idEstudiante: "+ rowSet.getInt(1));
System.out.println("nombre: "+ rowSet.getString(2));
System.out.printf("nota: %.1f", rowSet.geInt(3)/10.);
}
}
// Bloque catch para manejar las excepcionescatch (Exception e) {
// Imprimir y mostrar la excepción junto con// el número de línea usando el método printStackTrace() e.printStackTrace();
}
}
}
Métodos principales de la interfaz JdbcRowSet que no heredan de RowSet
Ejemplo de selección parametrizada JdbcRowSet:
JdbcRowSetImpl jrs =new JdbcRowSetImpl();
jrs.setCommand("SELECT * FROM TITLES WHERE TYPE = ?");
jrs.setURL("jdbc:myDriver:myAttribute");
jrs.setUsername("cervantes");
jrs.setPassword("sancho");
jrs.setString(1, "BIOGRAPHY");
jrs.execute();
commit(): hace que todos los cambios realizados en este objeto RowSet desde la última llamada al método commit sean permanentes y los escribe en la base de datos.
Queremos desarrollar una aplicación para una biblioteca y necesitamos interactuar con una base de datos que contiene información sobre los libros que tenemos en nuestra colección de libros.
Para ello, vamos a crear:
Clase Book: representa la entidad libro.
Clase BookDAO: permite realizar operaciones CRUD (Create, Read, Update y Delete) sobre la tabla Book en la base de datos.
Clase ConnectionManager: para la gestión y obtención de las conexiones a la base de datos de una manera eficiente. Emplearemos el patrón Singleton para el gestor de conexiones, que en la primera versión tendrá una única conexión, pero que podremos convertir en un conjunto/pool de conexiones.
Estructura de la base de datos
Está formada por una única tabla, Book. La tabla Contido no se usará de momento.
equals y hashCode: dos libros son iguales si tienen el mismo ISBN. Además, el método hashcode debe devolver un valor coherente con el método equals (todos los objetos iguales deben tener, al menos el mismo hashCode).
toString: Devuelve el título, el autor y el año. Si no está disponible, añade un asterisco.
Esta interface será implantada por todas aquellas clases DAO que trabajen con objetos con imágenes. Los nombres de los métodos son totalmente descriptivos:
Tiene como atributo final un objeto de tipo Connection, con, que recoge como argumento el constructor:
publicBookDAO(Connection con) {
this.con= con;
}
Atributos
Objeto Connection, pasado al constructor.
Métodos
get(long idBook): devuelve un objeto Book con la información del libro que tiene el identificador pasado como parámetro.
getAll(): devuelve una lista de todos los libros almacenados en la base de datos.
save(Book book): crea un nuevo registro en la tabla Book con la información del libro pasado como parámetro. Importante: además, debe guardar el idBook en el objeto, por lo que es necesario obtener el ID del registro insertado.
Debe crearse la sentencia con la opción Statement.RETURN_GENERATED_KEYS.
update(Book book): actualiza la información del registro correspondiente al libro pasado como parámetro.
delete(Book book): elimina el registro correspondiente al libro con el identificador del libro pasado como parámetro.
deleteById(long idBook): elimina el registro correspondiente al libro con el identificador pasado como parámetro.
getAllIds(): devuelve una lista con los ids de todos los libros de la base de datos.
updateLOB(Book b, String f):actualiza el libro en la base de datos con el contenido del archivo recogido como parámetro. Usa setBinaryStream.
Ayuda: puedes emplear el método setBinaryStream de PreparedStatement), previamente habiendo leído los bytes.
updateLOBByID(long b, String f): actualiza el libro con el id recogido como por parámetro con el contenido del archivo recogido como parámetro.
deleteAll(): borra todos los libros.
Debes implantar la gestión de sentencias de esta la clase BookDAO por medio de try-with-resources para manejar los cierres de los Statement y los ResultSet de consultas automáticamente.
La conexión no debe cerrarse, pues debe permanecer abierta para futuros usos.
Clase ContidoDAO implementa DAO<Contido>
AppBiblioteca
a) Haz una aplicación que haga uso el ConnectionManager para obtener una conexión y se la pase al constructor de BooKDAO.
Crea varios libros y añádelos a la base de datos, incluyendo las portadas de los libros desde la aplicación.
Usa ConnectionManager para obtener una conexión y pasarla al constructor de BookDAO.
Crea varios libros y añádelos a la base de datos.
Ejemplo de libros:
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780307277672', 'Cien años de soledad', 'Gabriel García Márquez', 1967, TRUE, NULL);
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780743273565', 'Harry Potter y la piedra filosofal', 'J.K. Rowling', 1997, TRUE, NULL);
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780307277672', 'Cien años de soledad', 'Gabriel García Márquez', 1967, TRUE, NULL);
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780743273565', 'Harry Potter y la piedra filosofal', 'J.K. Rowling', 1997, TRUE, NULL);
VALUES ('9780307959474', 'The Sense of an Ending', 'Julian Barnes', 2011, TRUE, NULL);
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780307386672', 'No Country for Old Men', 'Cormac McCarthy', 2005, TRUE, NULL);
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9781400064168', 'The Road', 'Cormac McCarthy', 2006, TRUE, NULL);
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780099590088', 'The Noise of Time', 'Julian Barnes', 2016, TRUE, NULL);
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780307277672', 'All the Pretty Horses', 'Cormac McCarthy', 1992, TRUE, NULL);
INSERTINTOPUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780099590088', 'Levels of Life', 'Julian Barnes', 2013, TRUE, NULL);
Interface gráfica
Crea una aplicación siguiendo la estructura de MVC con el modelo de datos anterior.
Puedes crear una vista o emplear la proporcionada en los apuntes:
El objeto del proyecto es diseñar e implantar un modelo de datos adecuado para una aplicación, tanto para dispositivos móviles como para escritorio.
El resultado final será un modelo de datos compuesto por varias clases, los adaptadores necesarios para trabajar con archivos o la base de datos y una interfaz de usuario que permita interactuar con los datos.
El lenguaje de programación empleado será Kotlin o Java, según se considere más adecuado para el proyecto elegido.
Interoperatividad entre Kotlin y Java
Kotlin es un lenguaje que se puede utilizar de forma interoperable con Java, lo que significa que se pueden mezclar ambos lenguajes en un mismo proyecto. Esto permite a los desarrolladores aprovechar las ventajas de ambos lenguajes y utilizarlos en conjunto en sus aplicaciones.
Incluso en Android Studio, se puede convertir código Java a Kotlin de forma automática o crear el modelo de datos en Java y el resto de la aplicación en Kotlin.
Hazlo como mejor te convenga, pero asegúrate de que el código sea coherente y siga las convenciones de codificación del lenguaje elegido.
El proyecto debe cumplir la siguiente estructura:
Persistencia de datos: se debe implantar la persistencia de datos en la aplicación, utilizando archivos o una base de datos según se considere más apropiado para el proyecto elegido.
El formato de los archivos puede ser:
Binario.
Texto.
Archivo de texto en formato JSON (locales o remotos).
La base de datos puede ser: Postgres, H2, SQLite o cualquier otra base de datos que consideres adecuada.
En el caso de aplicaciones móviles, se debe tener en cuenta la gestión de la persistencia de datos en el contexto de una aplicación móvil, utilizando el patrón de acceso a datos adecuado para la plataforma: ViewModel (para la gestión de la UI), LiveData o StateFlow (para la observación de datos), Room (con SQLite y patrón Repository), Retrofit (para acceso a servicios web), etc.
También pueden utilizarse plataformas BaaS (Backend as a Service) como Firebase, Supabase, etc.
Para Firebase, se puede emplear la librería de Firebase para Android, que proporciona una API sencilla para interactuar con la base de datos en tiempo real y el sistema de autenticación de Firebase:
Modelo de datos: se debe un modelo de datos que represente la información que se va a manejar en la aplicación. Este modelo debe ser coherente y adecuado para el propósito de la aplicación, siguiendo los estándares de nombrado en Kotlin o Java. Además, debe incluir los adaptadores de Gson en el caso de emplear JSON.
Patrones de acceso a datos: se deben implementar los patrones de acceso a datos necesarios para interactuar con los datos almacenados. Esto puede incluir la creación de clases de acceso a datos, adaptadores, o cualquier otro componente necesario para gestionar la persistencia de datos.
Como se ha comentado anteriormentte, en el caso de aplicaciones móviles, se debe tener en cuenta la gestión de la persistencia de datos en el contexto de una aplicación móvil, utilizando el patrón de acceso a datos adecuado para la plataforma: ViewModel (para la gestión de la UI), LiveData o StateFlow (para la observación de datos), Room (con SQLite y patrón Repository), Retrofit (para acceso a servicios web), etc.
En el caso de aplicaciones de escritorio, se debe tener en cuenta la gestión de la persistencia de datos en el contexto de una aplicación de escritorio, utilizando el patrón de acceso a datos adecuado para la plataforma: DAO (Data Access Object), JDBC (Java Database Connectivity), etc.
Interfaz de usuario: aunque la parte importante es la parte del modelo y del patrón de arquitectura empleado, se debe implementar una interfaz de usuario que permita interactuar con los datos almacenados.
En el caso de aplicaciones móviles, se debe tener en cuenta la gestión de la interfaz de usuario en el contexto de una aplicación móvil, utilizando los componentes de la interfaz de usuario adecuados para la plataforma: Activities, Fragments, Views, etc.
En el caso de aplicaciones de escritorio, se debe tener en cuenta la gestión de la interfaz de usuario en el contexto de una aplicación de escritorio, utilizando los componentes de la interfaz de usuario adecuados para la plataforma: JFrame, JPanel, JDialog, etc. No es necesario que siga el patrón MVC, aunque se valorará positivamente si se ha seguido.
Persistencia de datos
Para la persistencia de datos, se debe elegir una de las siguientes opciones:
Persistencia en archivos: se pueden utilizar archivos para almacenar los datos de la aplicación. Los archivos pueden ser de texto, binarios o JSON, y pueden ser almacenados localmente en el dispositivo o de forma remota en un servidor. También pueden emplearse API Rest libres o disponibles en la red, como los ejemplos con los que hemos trabajado en clase. Ejemplos:
Almacenar los datos de una lista de tareas pendientes en un archivo de texto o JSON.
Almacenar los datos de una lista de contactos en un archivo binario.
Consulta de API de películas o libros para obtener información y/o almacenarla localmente.
En el caso de emplear JSON deben crearse serializadores o deserializadores personalizados por medio de Gson.
En Android debe emplearse la clase File para la gestión de archivos y en Java la clase FileReader y FileWriter.
Para acceso a las API Rest, en Android se empleará Retrofit y en Java la clase HttpURLConnection, usando la clases con buffer.
Para Android el API Rest debe estar separada de la vista por medio del patrón MVVM (Model-View-ViewModel) y en Java por medio del patrón DAO (Data Access Object):
En la actualidad se emplea el patrón MVVM en Android, que se basa en la separación de la lógica de negocio de la vista, permitiendo una mayor modularidad y reutilización del código. En Java se emplea el patrón DAO (Data Access Object) para la separación de la lógica de acceso a datos de la lógica de negocio.
Para la retención de datos en Android por medio de MVVM se emplea la clase ViewModel y LiveData para la observación de datos. En Java se emplea la clase DAO para la gestión de la persistencia de datos. Sin embargo, existe un aproximación más actual para
Ejemplo ViewModel con Retrofit
Ejemplo ViewModel con Retrofit en una aplicación Android siguiendo el patrón MVVM (Model-View-ViewModel).
Define una clase de datos que represente la respuesta de la API:
dataclassUser(
val id: Int,
val name: String,
val email: String
)
3. Configuración Retrofit
Configura Retrofit para realizar las llamadas a la API:
interfaceApiService {
@GET("users")
suspendfungetUsers(): List<User>
// Si se le quiere pasar un paámetro a la API
@GET("users/{id}")
suspendfungetUser(@Path("id") id: Int): User
}
// Configuración de Retrofit
objectRetrofitClient {
// Esta API es un ejemplo, puedes reemplazarla por la que necesites
// https://jsonplaceholder.typicode.com/ es un servicio de prueba gratuito
privateconstval BASE_URL = "https://jsonplaceholder.typicode.com/"val apiService: ApiService by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()) // Deberías personalizarlo según tus necesidades
.build()
retrofit.create(ApiService::class.java)
}
}
4. Creación el repositorio
El repositorio se encarga de obtener los datos de la API:
El ViewModel se comunica con el repositorio y expone los datos a la vista:
classUserViewModel : ViewModel() {
privateval userRepository = UserRepository()
privateval _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> get() = _users
init {
fetchUsers()
}
privatefunfetchUsers() {
viewModelScope.launch { // Corrutina para llamada a la API en el hilo del viewModel
try {
val userList = userRepository.getUsers()
_users.postValue(userList) // notificar a la vista que los datos han cambiado
} catch (e: Exception) {
// Manejar el error
}
}
}
}
6. Conectar el ViewModel con la vista
Finalmente, conecta el ViewModel con tu actividad o fragmento:
classUserActivity : AppCompatActivity() {
privatelateinitvar userViewModel: UserViewModel
overridefunonCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
userViewModel.users.observe(this, Observer { users ->// Actualizar la UI con la lista de usuarios
})
}
}
Este es un ejemplo básico para empezar. Puedes expandirlo añadiendo manejo de errores más robusto, pruebas unitarias, y otras mejoras según tus necesidades.
Ejemplo de uso de Retrofit en Android con MVVM con corrutinas y LiveData para una API de películas:
/* El servicio de Retrofit se declara con una interfaz que define los métodos de * la API
* En este caso getPopularMovies() para obtener las películas populares devuelve un objeto de tipo Response<PeliculaResponse>, que permite gestionar la respuesta de la API y errores.
* */interfacePeliculasService {
@GET("movie/popular")
suspendfungetPeliculasPopulares(
@Query("key") claveAPI: String
): Response<PeliculaResponse>
}
// El repositorio de películas se encarga de llamar al servicio de Retrofit y gestionar la respuesta (no he creado la clase intermedia RetrofitClient)
// A diferencia del caso anterior en el que se emplea un objeto RetrofitClient, aquí se inyecta el servicio en el constructor del repositorio
classPeliculaRepository(privateval servicioPeliculas: PeliculasService) {
// El identificado "suspend" indica que la función debe ser llamada desde una corrutina
// y que no bloqueará el hilo principal
suspendfungetPelicualsPopulares(): Response<PeliculaResponse> {
return servicioPeliculas.getPeliculasPopulares("tuvalordelaclave")
}
}
// La respuesta de la API se mapea a un objeto de datos
dataclassPeliculaResponse(val results: List<Movie>)
// Objeto de datos de película
dataclassMovie(val title: String, val overview: String, val posterPath: String)
// El ViewModel de películas se encarga de gestionar la lógica de la vista y la llamada a la API
// y de exponer los datos a la vista
classMovieViewModel(privateval peliculaRepository: PeliculaRepository) : ViewModel() {
privateval _popularMovies = MutableLiveData<List<Movie>>()
val popularMovies: LiveData<List<Movie>> = _popularMovies
fungetPopularMovies() {
viewModelScope.launch { // Corrutina para llamada a la API en el hilo del viewModel
val response = peliculaRepository.getPelicualsPopulares()
if (response.isSuccessful) {
_popularMovies.value = response.body()?.results
}
}
}
}
La actividad principal debe tener un objeto de la clase MovieViewModel y una lista de objetos de la clase Movie, además de las vistas de la interfaz de usuario:
classMainActivity : AppCompatActivity() {
privatelateinitvar movieViewModel: MovieViewModel
overridefunonCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Inicializar el ViewModel y pasarle una instancia del repositorio:
val retrofit = Retrofit.Builder()
.baseUrl("https://api.themoviedb.org/3/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val peliculasService = retrofit.create(PeliculasService::class.java)
val peliculaRepository = PeliculaRepository(peliculasService)
movieViewModel = ViewModelProvider(this, MovieViewModelFactory(peliculaRepository)).get(MovieViewModel::class.java)
movieViewModel.getPopularMovies()
// Observar los cambios en la lista de películas populares
movieViewModel.popularMovies.observe(this, Observer { movies ->// Actualizar la interfaz de usuario con la lista de películas
val adapter = MovieAdapter(movies)
recyclerView.adapter = adapter
})
}
}
Más información sobre Retrofit en Android: Retrofit
Más información sobre corrutinas en Android: Corrutinas
Más información sobre LiveData en Android: LiveData
StateFlow, FLow vs LiveData
En la actualidad, se recomienda el uso de StateFlow en lugar de LiveData para la observación de datos en Android, ya que StateFlow es más flexible y permite una gestión más eficiente de los datos en la aplicación. Sin embargo, el uso de LiveData sigue siendo válido y es una opción viable para la observación de datos en Android.
StateFlow y LiveData tienen similitudes. Ambas son clases contenedoras de datos observables y siguen un patrón similar cuando se usan en la arquitectura de una app.
Sin embargo, ten en cuenta que StateFlow y LiveData se comportan de manera diferente:
StateFlowrequiere que se pase un estado inicial al constructor, mientras que LiveData, no.
LiveData.observe()cancela automáticamente el registro del consumidor cuando la vista pasa al estado STOPPED, mientras que la recopilación de StateFlow o cualquier otro flujo, no deja de recopilar automáticamente. Para obtener el mismo comportamiento, debes recopilar el flujo desde un bloque Lifecycle.repeatOnLifecycle.
Persistencia en base de datos: se pueden utilizar bases de datos para almacenar los datos de la aplicación. Las bases de datos pueden ser locales o remotas, y pueden ser de distintos tipos (SQLite, Postgres, MySQL, etc.). También se pueden utilizar servicios de bases de datos en la nube. Ejemplos:
Almacenar los datos de una lista de tareas pendientes en una base de datos SQLite.
Almacenar los datos de una lista de contactos en una base de datos Postgres.
Consulta de API de películas o libros para obtener información y almacenarla en una base de datos local o remota.
En Android se empleará Room con SQLite o, menos recomendable, driver JDBC compatibles con Android, y en Java JDBC con MySQL o Postgres.
En Android se empleará el patrón Repository para la gestión de la base de datos y en Java el patrón DAO (Data Access Object).
En Android se empleará el patrón MVVM para la gestión de la interfaz de usuario y en Java el patrón MVC.
Ejemplo con Jetpack Compose y StateFlow
Con Jetpack Compose, el enfoque cambia un poco, pero los principios de usar ViewModel y LiveData (o StateFlow) siguen siendo útiles para manejar el estado de la UI de manera reactiva y eficiente.
Jetpack Compose y ViewModel
En Jetpack Compose, puedes seguir usando ViewModel para manejar la lógica de negocio y el estado de la UI.
Sin embargo, en lugar de LiveData, es común usar State y StateFlow para una integración más fluida con Compose.
1. Dependencias
Debes tener las siguientes dependencias en tu archivo build.gradle:
Reactividad: StateFlow es una API de flujo de datos que es inherentemente reactiva, lo que se integra perfectamente con el paradigma de Compose.
Integración fluida: collectAsState convierte un StateFlow en un State de Compose, lo que facilita la actualización de la UI en respuesta a cambios de datos.
Manejo del ciclo de vida: Al igual que LiveData, StateFlow respeta el ciclo de vida de los componentes de la UI, evitando fugas de memoria y actualizaciones innecesarias.
Puedes usar LiveData con Jetpack Compose, StateFlow ofrece una integración más natural y eficiente con el paradigma declarativo de Compose.
La persistencia consiste en almacenar los datos de forma permanente.
La persistencia se puede realizar mediante ficheros (planos, XML, JSON,…) o sistemas de base de datos (relacionales, orientados a objetos, JSON, XML, etc.).
En esta unidad vamos a estudiar el almacenamiento en bases de datos relacionales por medio de mapeo objeto-relacional (ORM)
y su implementación en Java mediante JPA con Hibernate o EclipseLink, entre otross.
El uso de ficheros se recomienda en pocos casos, como por ejemplo, para almacenar datos de configuración de la aplicación.
Entre las desventajas están:
Redundancia de datos: puede haber datos duplicados en diferentes ficheros.
Complejidad de acceso a datos: un cambio en los datos puede requerir cambios en la aplicación.
Seguridad: en un conjunto de ficheros es más complicado establecer permisos, en SGBD se implantan de forma nativa.
Concurrencia: ae precisa establecer un sistema de bloqueo de ficheros para evitar que dos usuarios accedan al mismo tiempo a un fichero.
Integridad de datos: no se pueden establecer restricciones de integridad referencial. Que viene impuesta por la aplicación.
No se puede realizar consultas complejas ni por índices.
Persistencia en Sistemas de Bases de Datos
Pueden utilizarse diferentes tipos de bases de datos:
Bases de datos relacionales: son las más utilizadas. Se basan en el modelo relacional de datos.
Los datos se almacenan en tablas y se relacionan entre sí mediante claves primarias y foráneas.
Ejemplos son:
Databricks. Databricks es el nombre de la plataforma analítica de datos basada en Apache Spark desarrollada por la compañía con el mismo nombre. La empresa se fundó en 2013 con los creadores y los desarrolladores principales de Spark. Permite hacer analítica Big Data e inteligencia artificial con Spark de una forma sencilla y colaborativa. Esta plataforma está disponible como servicio cloud en Microsoft Azure y Amazon Web Services (AWS)
JDBC (Java Database Connectivity) Nativa: es una API de Java que permite ejecutar sentencias SQL y procedimientos almacenados en un SGBD.
DAO (Data Access Object): es un patrón de diseño que permite separar la lógica de negocio de la lógica de acceso a datos.
Cada clase del modelo de datos tiene su clase DAO asociada con método para realizar las operaciones CRUD (Create, Read, Update, Delete).
Frameworks de persistencia/ORM (Object/Relational Mapping): son librerías que permiten realizar la persistencia de datos de forma transparente.
JPA (Java Persistence API): es una especificación de una API de Java que permite mapear objetos Java a tablas de una base de datos relacional.
Implementaciones de JPA o nativas: existen varias implementaciones de la especificación JPA, pero la más popular es
Hibernate, un framework de persistencia que implementa la especificación JPA. Otras:
Mapeo Objeto-Relacional (ORM) es el proceso de convertir objetos Java en tablas de bases de datos.
Esto permite interactuar con una base de datos relacional sin necesidad de utilizar SQL.
Jakarta/Java Persistence API (JPA) es una especificación que define cómo persistir datos en aplicaciones Java. El enfoque principal de JPA es la capa de ORM.
Hibernate es uno de los frameworks de ORM más populares en uso hoy en día y una
implementación estándar de la especificación JPA, con algunas características adicionales específicas de Hibernate.
Su primera versión se lanzó hace casi
veinte años y aún cuenta con un excelente soporte de la comunidad y lanzamientos regulares.
En esta unidad nos centraremos en Jakarta Persistence API (JPA) con Hibernate, aunque también veremos alguna otra implementación de referencia de JPA,
como EclipseLink o DataNucleus. SpringBoot Data JPA utiliza Hibernate como implementación de JPA, pero, en cuanto a rendimiento, no es la mejor opción.
Desde los primeros días de la plataforma Java, han existido interfaces de programación para proporcionar pasarelas hacia la base de datos y para abstraer las necesidades de persistencia específicas del dominio de las aplicaciones empresariales.
Jakarta Persistence define un estándar para la gestión de persistencia y el mapeo objeto/relacional en entornos Java basado en POJO (Plain Old Java Object) para la persistencia en Java.
Jakarta Persistence es sólo una especificación que no puede realizar la persistencia por sí misma.
Por supuesto, Jakarta Persistence requiere una base de datos para persistir.
El API Jakarta para la gestión de persistencia y el mapeo objeto/relacional puede emplearse en Jakarta EE o Java SE.
En la actualidad, existen varias soluciones de persistencia en Java. Nos centraremos en la especificación JPA y soluciones propietarias como Hibernate, EclipseLink, DataNucleus, etc.
La API de Persistencia de Jakarta consta de cuatro áreas:
La Jakarta Persistence API (JPA)
La API de Criterios de Persistencia de Jakarta (Jakarta Persistence Criteria API)
El Lenguaje de Consulta de Persistencia de Jakarta (JPQL,Jakarta Persistence Query Language)
Metadatos de mapeo objeto-relacional
1.1. Historia
La API de Java Persistence (JPA) es una especificación de Java EE que describe cómo administrar datos relacionales en
aplicaciones empresariales de Java. La API de JPA se basa en la especificación de Java Data Objects (JDO),
especificación de Java EE que describe cómo administrar datos en aplicaciones empresariales de Java.
JPA 1.0: la fecha de lanzamiento final de la especificación JPA 1.0 fue el 11 de mayo de 2006 como parte del Java Community
Process JSR 220.
JPA 2.0 se lanzó el 10 de diciembre de 2009 (la plataforma Java EE 6 requiere JPA 2.0).
JPA 2.1 se lanzó el 22 de abril de 2013 (la plataforma Java EE 7 requiere JPA 2.1).
JPA 2.2 se lanzó en el verano de 2017.
JPA 2.3 se lanzó en el verano de 2019.
JPA 3.0 se lanzó en el verano de 2020. Fue renombrada a Jakarta Persistence 3.0 (requiere Java 8). Así, todos los paquetes
se renombraron de javax.persistence a jakarta.persistence. Implementaciones:
Hibernate (desde versión 5.5)
EclipseLink (desde versión 3.0)
DataNucleus (desde versión 6.0)
JPA 3.1 se lanzó en la primavera de 2022 como parte de Jakarta EE 10 (requiere Java 11). Implementaciones:
La especificación Jakarta Persistence 3.1 es la primera versión con nuevas características y mejoras después de que
la especificación se trasladara a la Eclipse Foundation (jakarta.persistence).
Java Persistence API 2.0 (2009)
La segunda versión Java Persistence 2.0 en 2009.
Incluyó varias características que no estaban presentes en la primera versión:
Capacidades de mapeo adicionales.
Formas flexibles de determinar la forma en que el proveedor accedía al estado de la entidad.
Extensiones al Lenguaje de Consulta de Persistencia de Java (JPQL).
Nueva API de Criterios de Java, una forma programática de crear consultas dinámicas.
Java Persistence 2.1 (2013)
Java Persistence 2.1 en 2013 agregó algunas características:
Soporte para generación de esquemas.
Métodos de conversión de tipos.
Creación de gráficos de entidades y pasarlos a consultas, lo que se conoce comúnmente como restricciones de grupo de recuperación
en el conjunto de objetos devueltos.
Contextos de persistencia no sincronizados para operaciones conversacionales mejoradas.
Soporte para procedimientos almacenados.
Inyección en clases de escuchadores de entidades.
Mejoras en el lenguaje de consulta de Java Persistence, la API de criterios y en el mapeo de consultas nativas.
Java Persistence 2.2 (2017)
Java Persistence 2.2 fue publicada por Oracle en junio de 2017:
Métodos para recuperar los resultados de las consultas (Query) y consultas tipadas (TypedQuery) como flujos (streams).
Soporte para tipos básicos de Fecha y Hora de Java 8: java.time.LocalDate, java.time.LocalTime,
java.time.LocalDateTime, java.time.OffsetTime y java.time.OffsetDateTime.
Permitir que los convertidores de atributos admitan la inyección de CDI.
Actualización del mecanismo de descubrimiento del proveedor de persistencia.
Permitir que todas las anotaciones de Java Persistence se utilicen en metaanotaciones.
Jakarta Persistence 3.0, lanzada en 2020, fue el cambio al espacio de nombres del paquete jakarta.
Trasladó las API existentes del paquete javax.persistence al paquete jakarta.persistence.
Todas las propiedades que contienen javax como parte del nombre se renombran de manera que javax se reemplace con jakarta.
Actualización de los espacios de nombres del esquema para un archivo de configuración de unidad de persistencia
y un archivo XML de mapeo objeto-relacional.
Jakarta Persistence 3.1 (2021)
El lanzamiento de Jakarta Persistence 3.1 fue publicado por la Eclipse Foundation en diciembre de 2021.
En general, los cambios en Jakarta Persistence 3.1 incluyeron:
Estandarización de la función EXTRACT en el Lenguaje de Consulta de Persistencia de Jakarta.
Estandarización de la Generación de UUID para claves primarias.
Definición del nombre del módulo jakarta.persistence para la API de Persistencia de Jakarta para el Sistema de Módulos de Plataforma Java.
Permitir que las interfaces EntityManagerFactory y EntityManager extiendan la interfaz java.lang.AutoCloseable.
Actualizaciones editoriales y aclaraciones en la especificación.
Para obtener una lista completa de cambios, consulta la sección de Historial de Revisiones del Documento de
Especificaciones disponible en este enlace.
Jakarta Persistence, anteriormente conocida como Java Persistence API,
es una especificación de interfaz de programación de aplicaciones de Jakarta EE que describe la gestión
de datos relacionales en aplicaciones empresariales de Java.
Como se ha comentado, JPA abarca cuatro áreas:
La API en sí, definida en el paquete jakarta.persistence (javax.persistence para Jakarta EE 8 y versiones anteriores).
La API de Criterios de Persistencia de Jakarta (Jakarta Persistence Criteria API)
El Lenguaje de Consulta de Jakarta Persistence (JPQL; anteriormente Lenguaje de Consulta de Java Persistence) que permite realizar consultas a una base de datos relacional obteniendo colecciones de objetos.
Metadatos objeto/relacional: la configuración puede hacerse con anotaciones (@Id, @Entity,…) o mediante ficheros XML.
Características:
JPA es una especificación (no implementación) que facilita el mapeo objeto-relacional para gestionar datos relacionales en
aplicaciones Java.
No se puede utilizar JPA directamente. Deben emplearse implementaciones ORM como Hibernate,
EclipseLink, MyBatis (antes IBatis), DataNucleus,.. que emplean la especificación de JPA.
La última versión con implementaciones estables es la 3.1,
que se lanzó en la primavera de 2022 como parte de Jakarta EE 10 (requiere Java SE 11 o superior).
Algunas de las implementaciones compatibles con esta especificación son:
La mayoría de las herramientas ORM como Hibernate, MyBatis
(antes IBatis) o EclipseLink, que es la implementación de referencia,
implementan este estándar.
JPA proporciona soporte para trabajar directamente con objetos en lugar de utilizar declaraciones SQL.
Dispone de un fichero de configuración denominado persistence.xml.
Consejo
JPA define un proceso de inicio diferente, junto con un formato estándar de archivo de configuración llamado persistence.xml.
En entornos de Java™ SE, se requiere que el proveedor de persistencia (Hibernate, EclipseLink,…) localice cada archivo
de configuración de JPA en el classpath en la ruta META-INF/persistence.xml.
La API de Jakarta Persistence proporciona métodos para administrar la persistencia de objetos a un almacén de datos relacional.
La implementación de referencia para JPA es EclipseLink, pero existen otras que cubren las necesidades de los desarrolladores, como:
Hibernate: es una solución de Mapeo Objeto/Relacional (ORM) para programas escritos en Java
y otros lenguajes que admiten la JVM.
EclipseLink: es una solución de persistencia de objetos para Java.
Spring Data JPA: es una biblioteca de Spring que simplifica el acceso a
los sistemas de almacenamiento de datos relacionales. Se basa en la tecnología de acceso a datos de Spring y utiliza
las características de JPA para simplificar el acceso a los sistemas de almacenamiento de datos relacionales.
Apache OpenJPA: es una implementación de JPA que puede utilizarse como un almacén de
datos independiente o como una extensión de Apache Geronimo.
Oracle TopLink: es una solución de persistencia de
objetos para Java. Desarrollada por Oracle con licencia dual, tanto comercial como de código abierto que derivó en Eclipse Link.
Podría decirse que ya está descontinuado.
DataNucleus: es una solución de persistencia de objetos para Java, OSGi y la plataforma
de Google App Engine. Es una implementación de JDO y JPA.
Por supuesto, también es posible desarrollar una implementación propia de JPA.
Nos centraremos en Hibernate, que es una de las implementaciones más populares (y mejor) de JPA, pero podremos utilizar
cualquiera de las otras implementaciones.
Dependencias Maven
Se precisa la especificación de Jakarta Persistence API y la implementación.
Para Hibernate, la dependencia Maven sería:
<!-- Se precisa la dependencia de Hibernate y la de la API de Jakarta Persistence --><dependency><groupId>jakarta.persistence</groupId><artifactId>jakarta.persistence-api</artifactId><version>3.1.0</version></dependency><!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core --><dependency><groupId>org.hibernate.orm</groupId><artifactId>hibernate-core</artifactId><version>6.6.4.Final</version></dependency>
Ejercicio 01.01. Creación de un proyecto con JPA
Para crear un proyecto con JPA y Hibernate, se puede utilizar el asistente de creación de proyectos de Eclipse o IntelliJ IDEA, sin embargo con la versión Community de IntelliJ IDEA no se puede crear un proyecto con JPA a través del asistente.
Crea un proyecto Java Maven y añade las dependencias de Hibernate y la API de Jakarta Persistence.
2.3 Fichero de configuración persistence.xml
Los artefactos (y elementos de configuración) de la unidad de persistencia se suelen empaquetar en un “archivo de persistencia”.
Un archivo con formato JAR que contiene el archivo persistence.xml en el directorio META-INF y
los archivos de clase de entidad (clases de persistencia).
Para desplegar la aplicación se precisa situar el archivo de persistencia, las clases de aplicación que utilizan las entidades y los archivos JAR del proveedor de persistencia (JDBC) en el classpath cuando se ejecuta el programa.
La configuración que describe la unidad de persistencia se define en un archivo XML llamado META-INF/persistence.xml.
Cada unidad de persistencia tiene un nombre, por lo que cuando una aplicación de referencia desea especificar la configuración para una entidad, solo necesita hacer referencia al nombre de la unidad de persistencia que define esa configuración.
Un solo archivo persistence.xml puede contener una o más configuraciones de unidades de persistencia con nombres,
pero cada unidad de persistencia es independiente y distinta de las demás
Los ==únicos que necesitamos especificar para este ejemplo son name, transaction-type, class y properties=0.
Ejemplo de configuración con Hibernate y H2 en memoria habría que indicar el proveedor de persistenciaorg.hibernate.jpa.HibernatePersistenceProvider,
Hibernate, y la base de datos que se va a utilizar, que en el ejemplo H2 en memoria:
<?xml version="1.0" encoding="UTF-8"?><persistencexmlns="https://jakarta.ee/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"version="3.0"><persistence-unitname="com.javhoz.ad.jpa.example"transaction-type="RESOURCE_LOCAL"><description>Ejemplo de unidad de persistencia con Hibernate y H2 en memoria</description><!-- 1. El proveedor de persistencia ES OPCIONAL, pero se recomienda --><provider>org.hibernate.jpa.HibernatePersistenceProvider</provider><!-- Hibernate --><!-- para EclipseLink sería: <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> --><!-- 2. Escanea las clases y las detecta automáticamente. En caso contrario
habría que indicarlo con "class" --><exclude-unlisted-classes>false</exclude-unlisted-classes><properties><!-- propiedades de JPA: --><propertyname="jakarta.persistence.jdbc.driver"value="org.h2.Driver"/><!-- Si usamos la configuración de Hibernate: --><!-- <property name="hibernate.connection.driver_class" value="org.h2.Driver"/> --><propertyname="jakarta.persistence.jdbc.url"value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/><!-- Si usamos la configuración de Hibernate: --><!-- <property name="hibernate.connection.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/> --><propertyname="jakarta.persistence.jdbc.user"value="sa"/><!-- Si usamos la configuración de Hibernate: --><!-- <property name="hibernate.connection.username" value="sa"/> --><propertyname="jakarta.persistence.jdbc.password"value=""/><propertyname="jakarta.persistence.schema-generation.database.action"value="drop-and-create"/><!-- create, drop-and-create, none, drop --><propertyname="jakarta.persistence.lock.timeout"value="100"/><propertyname="jakarta.persistence.query.timeout"value="100"/><propertyname="jakarta.persistence.validation.mode"value="NONE"/><!-- propiedades de Específicas de Hibernate: --><propertyname="hibernate.archive.autodetection"value="class, hbm"/><propertyname="hibernate.dialect"value="org.hibernate.dialect.H2Dialect"/><propertyname="hibernate.connection.pool_size"value="50"/><propertyname="hibernate.show_sql"value="true"/><propertyname="hibernate.format_sql"value="true"/><!-- <property name="hibernate.hbm2ddl.auto" value="create-drop"/> <!– create-drop, update, create, validate –>--><propertyname="hibernate.max_fetch_depth"value="5"/><propertyname="hibernate.cache.region_prefix"value="hibernate.test"/><propertyname="hibernate.cache.region.factory_class"value="org.hibernate.testing.cache.CachingRegionFactory"/><!--NOTE: hibernate.jdbc.batch_versioned_data debe ponerse como "false" en Oracle --><propertyname="hibernate.jdbc.batch_versioned_data"value="true"/><propertyname="hibernate.service.allow_crawling"value="false"/><propertyname="hibernate.session.events.log"value="true"/></properties></persistence-unit></persistence>
Para Hibernate con MySQL con JPA 3.1, por ejemplo, sería:
Ejemplos de Dialectos de Hibernate 6,
que se pueden utilizar en la propiedad hibernate.dialect, y no son más que clases que implementan la interfaz Dialect:
Para cambiar de implementación de EclipseLink a Hibernate en la aplicación Java, sólo se precisa cambiar el fichero de configuración
de la aplicación, denominado persistence.xml. Sin embargo, Hibernate y EclipseLink tienen algunas características específicas que no están
incluidas en la especificación JPA. Por lo tanto, si utilizas estas características específicas, no podrás cambiar de implementación
y deberás utilizar la implementación específica, con los respectivos archivos de configuración:
hibernate.cfg.xml para Hibernate y
eclipselink.xml para EclipseLink.
Si se utiliza persistence.xml, la especificación sigue siendo la misma. Esa es la ventaja de utilizar JPA.
Ejercicio 01.02. Creación de un archivo de configuración de persistencia
Crea un directorio META-INF en el directorio src/main/resources y añade un archivo persistence.xml con la configuración de la unidad de persistencia con el nombre com.sanclemente.ad.jpa.exemplo.
El fichero de configuración persistence.xmldebe apuntar a una base de datos H2 en memoria. Además, debes añadir los Drivers de H2 para que la aplicación pueda conectarse a la base de datos:
Ten en cuenta que precisas crear la base de datos en memoria H2 y añadir las tablas necesarias, por lo que el parámetro jakarta.persistence.schema-generation.database.action debe ser “create”.
2.3. Entidades/Entity
Una entidad es una clase que representa un objeto persistente almacenado en una base de datos relacional.
Para que una clase sea una Entidad debe cumplir:
Debe ser una clase POJO (Plain Old Java Object): POJO es un objeto Java que no está sujeto a ninguna restricción
de las impuestas por la Especificación del lenguaje Java (sin herencias, implementaciones, dependencias de bibliotecas, etc. ). Sólo puede tener:
Atributos.
Constructores.
getters y setters (además de métodos de Object…)
Debe tener un constructor por defecto NO privado.
Puede tener constructores adicionales y declararse como abstracta.
No debe ser una clase interna (aunque puede ser una clase anidada estática).
No puede ser final.
Suelen implantar java.io.Serializable (aunque no es obligatorio en entornos SE).
Para convertirla en una entidad debe tener la anotación @Entity, declarada en jakarta.persistence.Entity.
Debe tener un identificador (ID) que se puede definir con la anotación @Id (declarada en jakarta.persistence.Id).
El identificador puede ser de cualquier tipo, aunque lo más habitual es que sea un tipo primitivo o un objeto de tipo java.lang.Long o java.lang.Integer.
Dicho identificador debe ser único para cada entidad y está asociado a la clave primaria de la tabla de la base de datos.
Ejemplo de declaración de una Entity/clase Persona:
Anotaciones para la clase (lo veremos más adelante al detalle):
@Entity: indica que la clase es una entidad. Elementos:
-name (String): el nombre de la entidad empleado en las consultas. Por defecto, el nombre de la clase (sin paquete). Por ejemplo: @Entity(name = "Persoa").
@Table: especifica el nombre de la tabla de la base de datos. Si no se indica, el nombre de la tabla es el nombre de la clase. Por ejemplo: @Table(name = “persona”). Elementos
name (String): el nombre de la tabla de la base de datos.
catalog (String): el nombre del catálogo de la base de datos.
schema (String): el nombre del esquema de la base de datos.
uniqueConstraints (de tipo UniqueConstraint[]): las restricciones de unicidad de la tabla de la base de datos.
indexes (Index[]): los índices de la tabla de la base de datos, para generación.
Anotaciones para los atributos: por defecto se mapean todos los atributos de la clase, pero se pueden excluir con la anotación @Transient.
@Id: Indica que el atributo es la clave primaria de la entidad.
@GeneratedValue: Indica que el valor del atributo es generado automáticamente por el sistema de persistencia. Posibles valores:
AUTO: El sistema de persistencia elige la estrategia de generación de claves primarias.
IDENTITY: El sistema de persistencia utiliza una columna de tipo autoincremental.
SEQUENCE: El sistema de persistencia utiliza una secuencia de base de datos.
TABLE: El sistema de persistencia utiliza una tabla adicional de base de datos.
UUID: El sistema de persistencia utiliza un UUID (JPA 3.1), identificador único universal, que es un número de 128 bits.
@Transient: Indica que el atributo no es persistente, es decir, no se almacena en la base de datos.
@Column: Indica que el atributo es una columna de la tabla de la base de datos. Permite definir el nombre de la columna, el tipo de datos, etc. Por ejemplo: @Column(name = “nombre”, nullable = false, length = 50):
name: Indica el nombre de la columna de la base de datos.
nullable: Indica si el atributo puede tener valores nulos (true) o no (false).
length: Indica la longitud máxima del atributo.
unique: Indica si el atributo debe ser único (true) o no (false).
insertable: Indica si el atributo se debe insertar en la base de datos (true) o no (false).
updatable: Indica si el atributo se debe actualizar en la base de datos (true) o no (false).
precision: Indica el número de dígitos de precisión de un atributo de tipo numérico.
scale: Indica el número de dígitos decimales de un atributo de tipo numérico.
…
Se mapean automáticamente los atributos de la clase con los campos de la tabla de la base de datos con el mismo nombre. Por ejemplo, el atributo nome se mapea con el campo nome de la tabla de la base de datos.
Los tipos admitidos son los siguientes:
Tipos envolventes de los tipos primitivos: Integer, Long, Float, Double, Boolean, Character, Byte, Short.
String.
java.util.Date.
java.util.Calendar.
java.sql.Date.
java.sql.Time.
java.sql.Timestamp.
java.math.BigDecimal.
java.math.BigInteger.
byte[].
java.util.UUID
java.time.LocalDate
java.time.LocalTime
java.time.LocalDateTime
java.time.OffsetTime
…
Los atributos que no se pueden mapear automáticamente con los campos de la tabla de la base de datos se deben excluir con la anotación @Transient y se deben mapear manualmente con la anotación @Column.
Datos temporales:
@Temporal: Indica que el atributo es un dato temporal (java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time,
java.sql.Timestamp). Por ejemplo: @Temporal(TemporalType.DATE). Así como @Temporal(TemporalType.TIME) y @Temporal(TemporalType.TIMESTAMP).
Ejercicio 01.03. Creación de una entidad
Crea una entidad Estudiante con idEstudiante (Long), nombre, apellidos, fechaDeNacimiento y dirección. Añade los atributos necesarios y las anotaciones para que sea una entidad. La clave primaria será idEstudiante de tipo autoincremental.
Ejercicio 01.04. Creación de una entidad
Crea una clase AppEstudiante que se conecte a la base de datos y añada un estudiante a la tabla de la base de datos.
Aunque lo veremos más adelante, lo que precisamos es crear un gestor de entidades e invocar al método persist para añadir un estudiante a la base de datos:
publicclassAppEstudiante {
publicstaticvoidmain(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("com.sanclemente.ad.jpa.exemplo");
EntityManager em = emf.createEntityManager();
Estudiante estudiante =new Estudiante("Juan", "Pérez", LocalDate.of(2000, 1, 1), "Calle Mayor, 1");
em.getTransaction().begin();
em.persist(estudiante);
em.getTransaction().commit();
// IMprime el estudiante para ver si se ha añadido correctamente y tiene un id em.close();
emf.close();
}
}
Para recuperarlo precisamos invocar al método find del gestor de entidades:
Estudiante estudiante = em.find(Estudiante.class, 1L); // Recupera el estudiante con id 1
2.4. Relaciones
Una relación es una “relación” entre dos entidades.
Puede ser unidireccional o bidireccional.
Una relación unidireccional tiene una entidad de origen y una entidad de destino.
Una relación bidireccional tiene una entidad de origen y una entidad de destino, pero también tiene una entidad de
destino y una entidad de origen.
Una relación bidireccional tiene dos lados: el lado propietario y el lado inverso. El lado propietario de una relación
bidireccional determina qué entidad de la relación se actualizará en la base de datos cuando se actualice la relación
en el código. El lado inverso de una relación bidireccional se actualiza automáticamente siempre que se actualice el
lado propietario.
2.5. Tipos de relaciones
Las relaciones entre entidades pueden ser de los siguientes tipos:
Uno a uno: una entidad de origen se asocia con una entidad de destino. Una entidad de destino también se asocia
con una entidad de origen. Por ejemplo, una persona tiene un pasaporte y un pasaporte pertenece a una persona.
Uno a muchos: una entidad de origen se asocia con una colección de entidades de destino. Una entidad de destino
se asocia con una entidad de origen. Por ejemplo, una persona tiene varias direcciones y cada dirección pertenece a
una persona.
Muchos a uno: una entidad de origen se asocia con una entidad de destino. Una entidad de destino se asocia con
una colección de entidades de origen. Por ejemplo, una dirección tiene una persona y una persona pertenece a varias
direcciones.
Muchos a muchos: una entidad de origen se asocia con una colección de entidades de destino. Una entidad de destino
se asocia con una colección de entidades de origen. Por ejemplo, una persona tiene varios teléfonos y un teléfono
pertenece a varias personas.
JPA significa Java Persistence API (Interfaz de Programación de Aplicaciones).
Fue lanzado inicialmente el 11 de mayo de 2006.
Es una especificación de Java que proporciona funcionalidad y estándares para herramientas de Mapeo Objeto-Relacional (ORM).
Se utiliza para examinar, controlar y persistir datos entre objetos Java y bases de datos relacionales.
Se considera como una técnica estándar para el Mapeo Objeto-Relacional.
Se le considera como un enlace entre un modelo orientado a objetos y un sistema de base de datos relacional.
Como es una especificación de Java, JPA no realiza ninguna funcionalidad por sí misma. Por lo tanto, necesita una implementación.
De este modo, para la persistencia de datos, herramientas ORM como Hibernate implementan las especificaciones de JPA.
Para la persistencia de datos, el paquete jakarta.persistence (antes javax.persistence) contiene las clases e interfaces de JPA.
JPA es solo una especificación, no es una implementación.
Es un conjunto de reglas y pautas para establecer interfaces para la implementación del mapeo objeto-relacional.
Necesita algunas clases e interfaces.
Admite un mapeo objeto-relacional simple, limpio y asimilado.
Admite polimorfismo e herencia.
Pueden incluirse consultas dinámicas y con nombre en JPA.
Hibernate
Es un Framework de Java, de código abierto, ligero y una herramienta de Mapeo Objeto-Relacional (ORM)
para el lenguaje Java que simplifica la construcción de aplicaciones Java para interactuar con la base de datos.
Se utiliza para guardar objetos Java en el sistema de base de datos relacional.
Hibernate es una implementación de que sigue el estándar de JPA.
Ayuda a mapear los tipos de datos Java a los tipos de datos SQL.
Contribuye a JPA.
Nota: El framework de Hibernate ORM fue inicialmente diseñado por Red Hat.
Se lanzó el 23 de mayo de 2007. Es compatible con JVM multiplataforma y está escrito en Java.
La característica principal de Hibernate es mapear las clases Java a tablas de base de datos.
JPA es una especificación. Proporciona funcionalidad y prototipo comunes para las herramientas ORM.
Todas las herramientas ORM (como Hibernate) siguen los estándares comunes, ejecutando la misma especificación.
Por lo tanto, si necesitamos cambiar nuestra aplicación de una herramienta ORM a otra, podemos hacerlo fácilmente.
Como sabemos, JPA es solo una especificación, lo que significa que no hay implementación.
Podemos anotar clases en la medida que queramos con anotaciones de JPA, aunque, nada sucederá sin una implementación.
Supongamos que JPA son las pautas que deben seguirse, sin embargo, Hibernate es un código de implementación de JPA
que une la API según lo descrito por la especificación de JPA y proporciona la funcionalidad anónima.
Diferencias entre JPA e Hibernate:
JPA
Hibernate
Está descrito en el paquete jakarta.persistence (+3.0) javax.persistence (2.3 o inferior).
Está descrito en el paquete org.hibernate.
Describe el manejo de datos relacionales en aplicaciones Java.
Hibernate es una herramienta de Mapeo Objeto-Relacional (ORM) que se utiliza para guardar objetos Java en un sistema de base de datos relacional.
No es una implementación, es solo una especificación de Java.
Hibernate es una implementación de JPA. Por lo tanto, sigue el estándar común proporcionado por JPA.
Es una API estándar que permite realizar operaciones en la base de datos.
Se utiliza para mapear tipos de datos Java con tipos de datos SQL y tablas de base de datos.
Utiliza Java Persistence Query Language (JPQL) como lenguaje de consulta orientado a objetos.
Utiliza Hibernate Query Language (HQL) como lenguaje de consulta orientado a objetos.
Utiliza la interfaz EntityManagerFactory para interactuar con la fábrica del administrador de entidades para la unidad de persistencia.
Utiliza la interfaz SessionFactory para crear instancias de sesión.
Utiliza la interfaz EntityManager para realizar acciones de crear, leer y eliminar para instancias de clases de entidad mapeadas.
Utiliza la interfaz Session para realizar acciones de crear, leer y eliminar para instancias de clases de entidad mapeadas.
Actúa como una interfaz de tiempo de ejecución entre una aplicación Java y Hibernate.
Actúa como una interfaz de tiempo de ejecución entre una aplicación Java y Hibernate.
La principal diferencia entre Hibernate y JPA es que Hibernate es un framework mientras que JPA son especificaciones de API.
Hibernate es la implementación de todas las pautas de JPA.
Hibernate se divide en varios módulos/artefactos bajo el grupo org.hibernate.orm.
El artefacto principal se llama hibernate-core.
<dependencies><!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
Pruebas unitarias --><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.10.1</version><scope>test</scope></dependency><!-- Dependencias para conexiones a bases de datos.
Sólo necesitamos la que vayamos a emplear. --><!-- https://mvnrepository.com/artifact/com.h2database/h2
Ojo con la versión. Si empleamos la versión 2.2.224 tendremos
que getionar la versión de Driver en DBeaver --><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.3.232</version></dependency><!-- https://mvnrepository.com/artifact/org.postgresql/postgresql --><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><version>42.7.4</version></dependency><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>9.1.0</version></dependency><!-- JPA --><dependency><groupId>jakarta.persistence</groupId><artifactId>jakarta.persistence-api</artifactId><version>3.1.0</version></dependency><!-- Implementaciones JPA. Usaremos una u otra.--><!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core --><dependency><groupId>org.hibernate.orm</groupId><artifactId>hibernate-core</artifactId><version>6.6.4.Final</version></dependency><!-- https://mvnrepository.com/artifact/org.eclipse.persistence/org.eclipse.persistence.jpa --><!-- <dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.jpa</artifactId>
<version>4.0.5</version>
</dependency>--></dependencies>
2. Creación del archivo de configuración persistence.xml
JPA define un proceso de arranque diferente al nativo de Hibernate, junto con un formato de archivo de configuración
estándar denominado persistence.xml. En entornos Java™ SE, se requiere que el proveedor de persistencia (Hibernate, EclipseLink, etc.)
ubique cada archivo de configuración JPA en la ruta de clases en la ruta META-INF/persistence.xml.
Añadidlo al directorio maven: src/main/resources/META-INF/persistence.xml.
2.1. Para Hibernate
Por ejemplo, para hibernate y h2:
<?xml version="1.0" encoding="UTF-8"?><persistencexmlns="https://jakarta.ee/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"version="3.0"><!--nombre único de la unidad de persistencia--><persistence-unitname="ejemplopersistenciaJPA"><description> Ejemplo de unidad de persistencia para Jakarta Persistence
</description><provider>org.hibernate.jpa.HibernatePersistenceProvider</provider><!-- <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>--><exclude-unlisted-classes>false</exclude-unlisted-classes><!-- Clases que se van a persistir --><!-- <class>com.javhoz.ad.orm.Usuario</class> --><!-- Propiedades de la unidad de persistencia --><properties><!-- Configuración de conexión a base de datos. H2 en memoria. --><propertyname="jakarta.persistence.jdbc.driver"value="org.h2.Driver"/><propertyname="jakarta.persistence.jdbc.url"value="jdbc:h2:E:/ruta/baseDatos;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO;DB_CLOSE_DELAY=-1"/><!-- <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1" />--><propertyname="jakarta.persistence.jdbc.user"value=""/><propertyname="jakarta.persistence.jdbc.password"value=""/><!-- crete: automáticamente, genera el esquema de la base de datos.
none: no hace nada (la base de datos debe existir)
create: crea las tablas (si no existen)
drop-and-create: borra las tablas y las vuelve a crear.
drop: borra las tablas cuando se cierra la factoría de persistencia, pero no las vuelve a crear.
--><propertyname="jakarta.persistence.schema-generation.database.action"value="create"/><!-- none, create, drop-and-create, drop --><!-- Muestra por pantalla las sentencias SQL --><propertyname="hibernate.show_sql"value="true"/><propertyname="hibernate.format_sql"value="true"/><propertyname="hibernate.highlight_sql"value="true"/><propertyname="hibernate.globally_quoted_identifiers"value="true"/><!-- <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />--></properties></persistence-unit></persistence>
El archivo persistence.xml se definen las propiedades de la base de datos, como el driver, la URL, el usuario y la contraseña. En el ejemplo anterior las propiedades y las etiquetas principales son:
provider: el proveedor de persistencia. En este caso, Hibernate con la clase: org.hibernate.jpa.HibernatePersistenceProvider.
jakarta.persistence.jdbc.driver: el driver de la base de datos.
jakarta.persistence.jdbc.url: la URL de la base de datos.
jakarta.persistence.jdbc.user: el usuario de la base de datos.
jakarta.persistence.jdbc.password: la contraseña del usuario de la base de datos.
jakarta.persistence.schema-generation.database.action: la acción a realizar sobre la base de datos. En este caso, se crean (create) las tablas de la base de datos.
hibernate.show_sql: muestra las sentencias SQL (propio de hibernate).
hibernate.format_sql: formatea las sentencias SQL (propio de hibernate).
hibernate.highlight_sql: resalta las sentencias SQL (propio de hibernate).
hibernate.globally_quoted_identifiers: permite el uso de comillas dobles en las sentencias SQL (pone los nombres de las tablas y columnas entre comillas dobles de manera automática) (propio de hibernate).
Precisamos la anotación @Entity para indicar que la clase Usuario es una entidad (obligatoria)
Precisamos la anotación @Id para indicar que el atributo id es la clave primaria (obligatoria)
Usamos la anotación @GeneratedValue para indicar que el valor de la clave primaria se genera automáticamente. Solo cuando las clave primarias son autogeneradas.
Se usa la anotación @Table para indicar que la tabla se llama usuarios (si no deseamos que se llame Usuario).
Ejercicio 03.01. Creación de una aplicación de persistencia de una biblioteca
Queremos desarrollar una aplicación para una biblioteca y necesitamos interactuar con una base de datos que contiene información sobre los libros que tenemos en nuestra colección.
Para ello, vamos a crear una clase Book que represente la entidad libro, la clase Contido y otra clase BookDAO que nos permita realizar operaciones básicas CRUD (Create, Read, Update y Delete) sobre la tabla Book en la base de datos.
Además, precisamos una clase BibliotecaJpaManager para la gestión y obtención de los objetos de tipo EntityManagerFactory de una manera eficiente. Emplearemos el patrón Singleton para el gestor BibliotecaJpaManager, que tenga un único objeto de tipo EntityManagerFactory y que nos permita obtener un objeto de tipo EntityManager para realizar las operaciones sobre la base de datos (queremos que el objeto de tipo EntityManagerFactory sea único para cada unidad de persistencia, para cada unidad de persistencia, no así el EntityManager, que podrá hacer varios para cada unidad de persistencia).
A) BASE DE DATOS (es la misma base de datos que hemos empleado en la unidad de bases de datos con JDBC):
Está formada por una tabla Book y una tabla Contido. La tabla Book tiene una estructura SIMILAR a la siguiente:
Columna
Tipo de dato
Descripción
idBook
int
Identificador único del ejemplar del libro
isbn
varchar(13)
Identificador del libro
titulo
varchar(100)
Título del libro
autor
varchar(100)
Autor del libro
anho
int
Año de publicación del libro
disponible
boolean
Indica si el libro está disponible
portada
Blob
Portada del libro en formato binario
dataPublicacion
Date
Fecha de publicación del libro
-- PUBLIC.Book definition
-- Drop table
-- DROP TABLE PUBLIC.Book;
CREATETABLEPUBLIC.Book (
idBook INTEGER NOTNULL AUTO_INCREMENT,
isbn CHARACTER VARYING(13) NOTNULL,
titulo CHARACTER VARYING(255) NOTNULL,
autor CHARACTER VARYING(255),
anho INTEGER,
disponible BOOLEAN DEFAULTTRUE,
portada BINARY LARGEOBJECT,
dataPublicacion DATE,
CONSTRAINT BOOK_PK PRIMARYKEY (idBook)
);
CREATEUNIQUEINDEX IdBookPK ONPUBLIC.Book (idBook);
CREATEINDEX IdxBookISBN ONPUBLIC.Book (isbn);
CREATEINDEX IdxBookTitle ONPUBLIC.Book (titulo);
CREATEUNIQUEINDEX PRIMARY_KEY_93 ONPUBLIC.Book (idBook);
La tabla Contido tiene una estructura SIMILAR a la siguiente:
Columna
Tipo de dato
Descripción
idContido
int
Identificador único del contenido del libro
idBook
int
Identificador del libro
contido
Blob
Contenido del libro en formato binario
*idBook es una clave foránea+ que referencia a la tabla Book.
El fichero persistencia.xml debe tener la siguiente configuración:
<?xml version="1.0" encoding="UTF-8"?><persistencexmlns="https://jakarta.ee/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"version="3.0"><persistence-unitname="bibliotecaH2"transaction-type="RESOURCE_LOCAL"><provider>org.hibernate.jpa.HibernatePersistenceProvider</provider><!-- <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>--><exclude-unlisted-classes>false</exclude-unlisted-classes><--falsesinoselistanlasclasesenelarchivodeconfiguración--><properties><!-- <property name="jakarta.persistence.jdbc.url" value="jdbc:mariadb://localhost:3306/peliculas"/>--><propertyname="jakarta.persistence.jdbc.url"value="jdbc:h2:rutaALaBaseDeDatos;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO"/><!-- Ejemplo con Access --><!--<property name="jakarta.persistence.jdbc.url" value="jdbc:ucanaccess://rutabase_base_datos.mdb"/>--><!-- <property name="jakarta.persistence.jdbc.user" value="root"/>--><!-- <property name="jakarta.persistence.jdbc.password" value=""/>--><propertyname="jakarta.persistence.jdbc.user"value=""/><propertyname="jakarta.persistence.jdbc.password"value=""/><!-- <property name="jakarta.persistence.jdbc.driver" value="net.ucanaccess.jdbc.UcanaccessDriver"/>--><propertyname="jakarta.persistence.jdbc.driver"value="org.h2.Driver"/><!-- Automáticamente, genera el esquema de la base de datos --><propertyname="jakarta.persistence.schema-generation.database.action"value="none"/><!-- Muestra por pantalla las sentencias SQL --><propertyname="hibernate.show_sql"value="true"/><propertyname="hibernate.format_sql"value="true"/><propertyname="hibernate.highlight_sql"value="true"/><!-- <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />--><!-- para HSQLDB y Ucanaccess --><propertyname="hibernate.dialect"value="org.hibernate.dialect.H2Dialect"/></properties></persistence-unit></persistence>
B) Clase BibliotecaJpaManager:
Mediante el patrón Singleton crea una clase BibliotecaJpaManager, mediante el patrón Singleton de manera que tenga un atributo emFactory de tipo EntityManagerFactory y que nos permita obtener un objeto de tipo EntityManager para realizar las operaciones sobre la base de datos.
Además, debe tener un método estático getEntityManager que devuelva un objeto de tipo EntityManager y que se encargue de crear el objeto EntityManager.
Hazlo con Thread-Safe y doble comprobación.
Reto: haz que la clase BibliotecaJpaManager tenga un singleton para cada factory, guardándolos en un mapa con el nombre de la unidad de persistencia como clave:
La lista de Contido es una lista de objetos de tipo Contido que representan los contenidos del libro. La clase Contido tiene los siguientes atributos: idContido y contido.
Ten en cuenta que existe en la base de datos una tabla Contido con los campos idContido y contido y una referencia al libro mediante una clave foránea idBook. De momento, no incluyas la List de contenidos en la clase Book, hazlos transient (bien con la anotación @Transient o con la palabra reservada transient), hasta que veamos las relaciones, que será @OneToMany.
Los métodos “set” de las propiedades deben devolver una referencia al propio objeto para poder encadenarlos.
IMPORTANTE: ten en cuenta que los atributos de la clase Book no coinciden con los campos de tabla por lo que debes refactorizar: author -> autor, ano -> anho, avaliable -> disponible, … o emplear la anotación @Column para mapear los atributos de la clase con los campos de la tabla.
Métodos de la clase Book (ya implantados):
Get y set para cada atributo.
setPortada (sin implantar): recibe File y lo asigna al atributo portada.
setPortada (sin implantar): recibe un array de bytes y lo asigna al atributo portada.
setPortada (Sin implantar): recibe un String con el nombre del fichero y lo asigna al atributo portada.
getImage: devuelve un objeto de tipo Image con la portada del libro.
public Image getImage() {
if (portada !=null) {
try (ByteArrayInputStream bis =new ByteArrayInputStream(portada)) {
return ImageIO.read(bis);
} catch (IOException e) {
}
}
returnnull;
}
equals y hashCode: considerando que son iguales cuando tienen el mismo isbn.
Además, el método hashCode debe devolver un valor coherente con el método equals (todos los objetos iguales deben tener, al menos el mismo hashCode).
toString: devuelve el título, el autor y el año. Si no está disponible escribe un asterisco.
D) Clase Contido implementa Serializable:
A diferencia de la clase empleada en la unidad de bases de datos con JDBC, la clase Contido no debe tener referencia al idBook, pues no es la mejor práctica (está hecho sólo a modo de ejemplo), debe tener, si queremos la relación bidireccional, una referencia a Book.
idContido: Long (autonumérico)
contido: String (contenido del libro en formato texto). Puedes hacer un atributo de tipo String o byte[] (para almacenar el contenido en formato binario), en cualquier caso, deberías modificar la tabla Contido en la base de datos.
Book book (relación con la clase Book)
Si has implantado la clase ContidoDao, debes modificar los métodos que obtienen el idBook del book:
contido.getBook().getIdBook();
E) Clase BookJPADao:
Esta clase, al igual que la clase BookDao, la clase BookJPADaodebe implantar la interface Dao<T>, de modo que tenga un objeto de tipo EntityManagercomo atributo. En sistemas empresariales, como la gestión de transacciones no se suele hacer por método, se guarda una referencia a la clase EntityManagerFactory y se gestiona por medio de try-with-resources para manejar los cierres de los EntityManager.
Dao<T>:
import java.util.List;
/**
*
* @author pepecalo
* @param <T> Tipo de dato del objeto
*/publicinterfaceDAO<T> {
T get(long id);
List<T>getAll();
voidsave(T t);
voidupdate(T t);
voiddelete(T t);
publicbooleandeleteById(long id);
public List<Integer>getAllIds();
publicvoidupdateLOB(T book, String f); // en BookJPADao recibe un objeto de tipo Book y un String con el nombre del ficheropublicvoidupdateLOBById(long id, String f);
voiddeleteAll();
}
Clase BookJPADao:
Implementa la interfaz DAO<Book> y gestiona las operaciones CRUD sobre la tabla Book de la base de datos.
Tiene como atributo un objeto de tipo EntityManager que recoge en el constructor.
Clase BookDAOFactory:
Factory de clases que implanten la interfaz DAO<Book>.
Implementa un método estático getBookDAO que recoge el tipo de DAO que se va a emplear y devuelve el objeto de tipo BookJPADAO. Sería interesante hacer cambios para que getBookDao recoja los parámetros necesarios como propiedades de la base de datos, nombre del archivo JSON, nombre de la unidad de persistencia, etc.
Ejecuta la aplicación para que haga uso del BookDaoFactory para obtener un objeto de tipo DAO<Book> para asignarlo al controlador de la aplicación. La aplicación debe funcionar igual que con JDBC, pero ahora con JPA.
Haz pruebas con los dos tipos de DAO. ¿Has notado alguna diferencia? Haz mejoras sobre el funcionamiento de la aplicación.
Puedes hacer pruebas de persistencia de libros en la base de datos:
Book libro =new Book("9788424937744", "Tractatus logico-philosophicus-investigaciones filosóficas", "Ludwig Wittgenstein", 2017, false);
libro =new Book("9788499088150", "Verano", "J. M. Coetzee", 2011, true);
Contexto de persistencia (Persistence Context): es conjunto de instancias de entidad gestionadas dentro de un gestor de entidades (EntityManager) en un momento dado.
Es necesario invocar una llamada de API específica antes de que una entidad se persista realmente en la base de datos.
Las llamadas de API para las operaciones en entidades, implementada por el gestor de entidades, se encapsula casi por completo dentro de una única interfaz jakarta.persistence.EntityManager, gestor de entidades al que se le delega el trabajo real de la persistencia.
Hasta que se utilice un gestor de entidades para crear, leer o escribir realmente una entidad, la entidad no es más que un objeto Java regular (no persistente). Se dice que ese objeto está gestionado por el EntityManager (gestor de entidades9.
Sólo puede existir una instancia Java con la misma identidad persistente en un contexto de persistencia en cualquier momento (con un único ID).
Consejo
Las implementaciones concretas de la interface EntityManager permiten leer y escribir en una base de datos específica, y ser implementadas por un proveedor de persistencia particular (o simplemente proveedor).
Es el proveedor es el que suministra el motor de implementación de respaldo para toda la API de Persistencia de Jakarta, desde el EntityManager hasta la implementación de las clases de consulta y la generación de SQL.
Para obtener un gestor de entidades, se debe crear una instancia de la fábrica de gestores de entidades, del tipo jakarta.persistence.EntityManagerFactory.
Cada EntityManager gestiona una unidad de persistencia. Una unidad de persistencia dicta de manera implícita o explícita la configuración y las clases de entidad utilizadas por todos los gestores de entidades obtenidos de la única instancia de EntityManagerFactory vinculada a esa unidad de persistencia.
Por lo tanto, hay una correspondencia uno a uno entre una unidad de persistencia y su instancia concreta de EntityManagerFactory.
Objetos, Clases y Conceptos de la API
Objeto
API
Descripción del Objeto
Persistence
Persistence
Clase de inicio utilizada para obtener una fábrica de gestores de entidades (EntityManagerFactory)
Entity Manager Factory
EntityManagerFactory
Objeto Factory configurado utilizado para obtener gestores de entidades (EntityManager)
Persistence Unit
–
Configuración con nombre que declara las clases de entidad y la información de la base de datos
Entity Manager
EntityManager
Objeto principal de la API utilizado para realizar operaciones y consultas en entidades
Persistence Context
–
Conjunto de todas las instancias de entidad gestionadas por un gestor de entidades específico
En el entorno de Java SE, podemos utilizar una clase de llamada Persistence invocando al
método estático createEntityManagerFactory() de la clase Persistence que devuelve el EntityManagerFactory
para el nombre de la unidad de persistencia especificado. Por ejemplo, para una unidad de persistencia llamada ServicioEmpleado:
El nombre de la unidad de persistencia especificada, “ServicioEmpleado”, pasado al método
createEntityManagerFactory(),
identifica la configuración de la unidad de persistencia dada que determina cosas como los parámetros de conexión
que los gestores de entidades creados a partir de ese objeto Factory utilizarán al conectarse a la base de datos.
Se puede obtener fácilmente un gestor de entidades de ella:
EntityManager em = emf.createEntityManager();
Un modo muy usual de crear un gestor de entidades es por medio de una clase Singleton:
Persistir una entidad es la operación de tomar una entidad transitoria, o una que aún no tiene ninguna representación
persistente en la base de datos, y almacenar su estado para que pueda ser recuperado más tarde.
Empleado emp =new Empleado(158); // Crea una instancia de la entidad Empleadoem.persist(emp);
Creamos un objeto de tipo Empleado configurando el ID, no el nombre ni el salario del Empleado.
Llamamos a persist() para iniciar la persistencia en la base de datos.
Si el gestor de entidades encuentra un error lanzará una excepción no verificada de tipo PersistenceException.
Cuando se completa la llamada a persist(), emp se convertirá en una entidad gestionada dentro del contexto
de persistencia del gestor de entidades.
Ejemplo de un método sencillo que crea un nuevo empleado y lo persiste en la base de datos.
public Empleado createEmpleado(int id, String nome, long salario) {
Empleado emp =new Empleado(id);
emp.setNome(nome);
emp.setSalario(salario);
em.persist(emp);
return emp;
}
3.2. Obtención de una entidad
Una vez que una entidad está en la base de datos, lo siguiente que normalmente se quiere hacer es obtenerla de nuevo:
<T> T find (Class<T> entityClass, Object primaryKey)
Recoge la clase de la entidad que se está buscando (Empleado), permite que el método find sea parametrizado y
devuelva un objeto del mismo tipo, y el objeto con ID o clave primaria que identifica la entidad en particular (con id 158).
Con esta información el gestor de entidades encuentra la instancia en la base de datos y el empleado que se devuelve
será una entidad gestionada, lo que significa que existirá en el contexto de persistencia actual asociado con el
gestor de entidades.
En el caso de que el objeto no se encuentre la llamada a find() simplemente devuelve null.
Debe realizarse una comprobación de nulos antes de la próxima vez que se utilice la variable emp.
Método de búsqueda:
public Empleado findEmpleado(int id) {
return em.find(Empleado.class, id);
}
3.3. Eliminación de una entidad
Aunque podría parecer lo contrario, el borrado (DELETE) de entidad de la base de datos no demasiado común.
Muchas aplicaciones nunca eliminan objetos, o si lo hacen, simplemente marcan los datos como obsoletos o ya no válidos
y los mantienen fuera de la vista de los clientes.
Para eliminar una entidad debe estar gestionada, debe estar presente en el contexto de persistencia.
La aplicación que realiza la llamada ya debería haber cargado o accedido a la entidad y ahora está emitiendo una
sentencia para eliminarla.
El método find() devuelve una instancia gestionada de Empleado, y luego se elimina la entidad usando la
llamada remove() en el gestor de entidades.
Si la entidad no se encuentra, entonces el método find() devolverá null, resultando una java.lang.IllegalArgumentException.
Se debe incluir una verificación de nulidad antes de llamar a remove():
Existen varias formas de actualizar una entidad, pero por ahora veremos el caso más simple y común, cuando se dispone
de una entidad gestionada y se desea realizar cambios en ella.
Si no tenemos una referencia a la entidad gestionada:
Debemos obtener la entidad una usando find().
Realizar operaciones de modificación en la entidad gestionada.
El siguiente código agrega 1000 euros al salario del empleado con un ID de 158 (yo ;-)):
No se llama al gestor de entidades para modificar el objeto, sino accediendo al objeto en sí.
Por esta razón, es importante que la entidad sea una instancia gestionada; de lo contrario, el proveedor de persistencia
no tendrá medios para detectar el cambio y no se realizarán cambios en la representación persistente del empleado.
public Empleado raiseSalarioEmpleado(int id, long cantidad) {
Empleado emp = em.find(Empleado.class, id);
if (emp !=null) {
emp.setSalario(emp.getSalario() + cantidad);
}
return emp;
}
Si no pudimos encontrar al empleado, devolvemos null para que el llamador sepa que no se pudo realizar ningún cambio.
Indicamos el éxito devolviendo al empleado actualizado.
4. Transacciones
En los ejemplos anteriores, no se ha hecho referencia a las transacciones, aunque los cambios en las entidades deben
hacerse persistentes mediante una transacción.
Excepto find(), asumimos que cada método estaba envuelto en una transacción.
La llamada a find() no es una operación de mutación, por lo que puede llamarse en cualquier momento, con o sin una transacción.
En estos ejemplos estamos empleando un entorno de Java SE, y el servicio de transacciones que debe usarse en Java SE
es jakarta.persistence.EntityTransaction necesitamos comenzar y confirmar la transacción en los métodos operativos,
o necesitamos comenzar y confirmar la transacción antes y después de llamar a un método operativo.
Inicio de la transacción:
En ambos casos, se inicia una transacción llamando a getTransaction() en el entity
manager para obtener la EntityTransactione invocando begin() en ella:
La clave del uso de transacciones es el entorno en el que se ejecuta el código.
La situación típica al ejecutarse dentro del entorno del contenedor Jakarta EE utiliza el API estándar de
Transacciones de Jakarta. El modelo de transacción cuando se ejecuta en el contenedor es asumir que la aplicación se
encargará de que exista un contexto transaccional cuando sea necesario.
Si no hay una transacción presente, entonces la operación de modificación lanzará una excepción o el cambio simplemente
no se persistirá en el almacén de datos.
5. Consultas
Una consulta es una solicitud de datos. En el contexto de JPA, una consulta es una solicitud de entidades.
Las consultas se pueden realizar de dos maneras:
Consultas dinámicas: se construyen en tiempo de ejecución como cadenas de consulta.
Consultas con nombre: se definen en tiempo de compilación como consultas con nombre.
5.1. Consultas dinámicas
Las consultas dinámicas se construyen en tiempo de ejecución como cadenas de consulta. Las cadenas de consulta son
sentencias de consulta en lenguaje de consulta de entidades (JPQL).
El lenguaje de consulta de entidades (JPQL) es un lenguaje de consulta orientado a objetos que se utiliza para definir
consultas de entidades y sus resultados.
Las consultas dinámicas se crean utilizando el método createQuery() en el gestor de entidades:
Query q = em.createQuery("SELECT e FROM Empleado e WHERE e.salario > 100000");
El método createQuery() toma una cadena de consulta JPQL y devuelve un objeto Query que se puede utilizar para
ejecutar la consulta y recuperar los resultados.
5.2. Consultas con nombre (estáticas)
Las consultas con nombre se definen en tiempo de compilación como consultas con nombre. Las consultas con nombre se
definen en un archivo de metadatos de la entidad o en un archivo de metadatos de consulta.
Las consultas con nombre se crean utilizando el método createNamedQuery() en el gestor de entidades:
El método createNamedQuery() toma el nombre de la consulta y devuelve un objeto Query que se puede utilizar
para ejecutar la consulta y recuperar los resultados.
Ejemplo de creación de una consulta con nombre:
@Entity@NamedQuery(name="findEmpleadoPorSalario", query="SELECT e FROM Empleado e WHERE e.salario > 100000")
publicclassEmpleado {
//...}
5.3. Ejecución de consultas
Una vez que se ha creado una consulta, se puede ejecutar utilizando el método getResultList() o getSingleResult():
TypedQuery<Empleado> q = em.createQuery("SELECT e FROM Empleado e WHERE e.salario > 100000", Empleado.class);
List<Empleado> results = q.getResultList();
Existen consultas tipadas y no tipadas. Las consultas tipadas devuelven un tipo específico de entidad, mientras que las consultas no tipadas devuelven un tipo de entidad genérico.
Con un consulta no tipada sería:
Query q = em.createQuery("SELECT e FROM Empleado e WHERE e.salario > 100000");
List results = q.getResultList();
Si la consulta no devuelve ningún resultado, getResultList() devuelve una lista vacía y getSingleResult() lanza
una excepción NoResultException. Si el resultado no es único y devuelve más de un resultado, getSingleResult()lanza una excepción NonUniqueResultException.
5. Consultas (ampliado)
En Jakarta Persistence, una consulta es similar a una consulta de base de datos, excepto que en lugar de utilizar
Structured Query Language (SQL) para especificar los criterios de la consulta, estamos consultando sobre entidades
y utilizando un lenguaje llamado Jakarta Persistence Query Language (Jakarta Persistence QL).
Una consulta se implementa en código como un objeto Query o TypedQuery<X>.
Se construye utilizando el EntityManager como fábrica.
La interfaz EntityManager incluye una variedad de llamadas a la API que devuelven un nuevo objeto Query o TypedQuery<X>.
Tipos de consultas en Jakarta Persistence
Una consulta puede definirse de forma estática o dinámica.
Una consulta estática (con nombre) se define típicamente en metadatos de anotación o XML, y debe incluir los criterios
de la consulta, así como un nombre asignado por el usuario. Este tipo de consulta también se llama consulta nombrada
y se busca posteriormente por su nombre en el momento de su ejecución.
Una consulta dinámica puede emitirse en tiempo de ejecución proporcionando los criterios de consulta de Jakarta Persistence QL
o un objeto de criterios. Pueden ser un poco más costosas de ejecutar porque el proveedor de persistencia no puede realizar
ninguna preparación de consulta de antemano, pero las consultas de Jakarta Persistence QL son, no obstante, muy simples
de usar y pueden emitirse en respuesta a la lógica del programa o incluso la lógica del usuario.
El siguiente ejemplo muestra cómo crear una consulta dinámica:
(Nota: por supuesto, esta puede no ser una consulta muy buena para ejecutar si la base de datos es grande y contiene
cientos de miles de empleados, pero sigue siendo un ejemplo adecuado):
Ejemplo usando getResultList:
TypedQuery<Empleado> query = em.createQuery("SELECT e FROM Empleado e", Empleado.class);
List<Empleado> emps = query.getResultList();
Ejemplo usando getResultStream:
TypedQuery<Empleado> query = em.createQuery("SELECT e FROM Empleado e", Empleado.class);
Stream<Empleado> employee = query.getResultStream();
Creamos un objeto TypedQuery<Empleado> emitiendo la llamada createQuery() en el EntityManager y pasando la
cadena de Jakarta Persistence QL que especifica los criterios de la consulta, así como la clase que debería ser
parametrizada en la consulta.
La cadena de Jakarta Persistence QL no se refiere a una tabla de base de datos EMPLEADO, sino a la entidad Empleado,
por lo que esta consulta selecciona todos los objetos Empleado sin filtrarlos más.
Para ejecutar la consulta, simplemente invocamos el método getResultList() o el método getResultStream() en ella.
El método getResultList() devuelve un List<Empleado> que contiene los objetos Empleado que coincidieron con los
criterios de la consulta. Observa que el List está parametrizado por Empleado, ya que el tipo parametrizado se propaga
desde el argumento de clase inicial pasado al método createQuery(). Podemos crear fácilmente un método que devuelva
todos los empleados.
El método getResultStream() devuelve un flujo del resultado de la consulta, por lo que, en este caso, devuelve el
flujo del resultado de la consulta Empleado. Por defecto, delega en getResultList().stream().
El método getResultStream() proporciona una mejor manera de moverse a través del conjunto de resultados de la consulta, ya que,
para conjuntos de datos grandes, evita leer todo el “conjunto de resultados” en memoria antes de que pueda usarse en la aplicación.
Método para Emitir una Consulta
public List<Empleado>findAllEmpleados() {
TypedQuery<Empleado> query = em.createQuery("SELECT e FROM Empleado e", Empleado.class);
return query.getResultList();
}
Con Stream:
public Stream<Empleado>findAllEmpleadosStream() {
TypedQuery<Empleado> query = em.createQuery("SELECT e FROM Empleado e", Empleado.class);
return query.getResultStream();
}
Ejercicio 04.01. Descarga y creación de la base de datos de JokeAPI
Dado el modelo de la aplicación de JokeAPI, en la que tenemos las enumeraciones Categoriam TipoChiste, Flag y la clase Chiste, vamos a crear una base de datos con JPA y los chistes de la API.
Enumeraciones
A) La enumeración Categoria tiene los siguientes valores:
Detalle de implementación de la enumeración Categoría
package com.javhoz.ad.chistes.model;
/**
* Updated by javhoz on 16/01/2025.
* <p>
* Enumeración de categorías de chistes.
* Pueden ser: Any, Misc, Programming, Dark, Pun, Spooky, Christmas
* Atributo: nombre, de tipo cadena.
*/publicenum Categoria {
ANY("Any"),
MISC("Misc"),
PROGRAMMING("Programming"),
DARK("Dark"),
PUN("Pun"),
SPOOKY("Spooky"),
CHRISTMAS("Christmas");
privatefinal String nombre;
Categoria(String nombre) {
this.nombre= nombre;
}
public String getNombre() {
return nombre;
}
/**
* Devuelve la categoría a partir de su nombre.
*
* @param nombre Nombre de la categoría
* @return Categoría
*/publicstatic Categoria getCategoria(String nombre) {
for (Categoria c : Categoria.values()) {
if (c.getNombre().equals(nombre)) {
return c;
}
}
returnnull;
}
/**
* Sobreescribe el método toString() para que devuelva el nombre de la categoría.
*
* @return Nombre de la categoría
* @see java.lang.Enum#toString()
*/@Overridepublic String toString() {
return nombre;
}
}
B) La enumeración TipoChiste contiene los siguientes valores:
Detalle de implementación de la enumeración TipoChiste
package com.javhoz.ad.chistes.model;
/**
* Updated by javhoz on 16/01/2025.
* Enumeración de tipos de chistes.
* Pueden ser: single, twopart
* Atributo: String nombre.
* Constructor: TipoChiste(String nombre)
* @see Categoria
* @see Flag
* @see Chiste
*
*/publicenum TipoChiste {
SINGLE("single"),
TWOPART("twopart");
privatefinal String nombre;
TipoChiste(String nombre) {
this.nombre= nombre;
}
public String getNombre() {
return nombre;
}
/**
* Devuelve el tipo de chiste a partir de su nombre.
* @param nombre Nombre del tipo de chiste
* @return Tipo de chiste
*/publicstatic TipoChiste getTipoChiste(String nombre) {
for (TipoChiste tc : TipoChiste.values()) {
if (tc.getNombre().equals(nombre)) {
return tc;
}
}
returnnull;
}
/**
* Sobreescribe el método toString() para que devuelva el nombre del tipo de chiste.
* @return Nombre del tipo de chiste
* @see java.lang.Enum#toString()
*/@Overridepublic String toString() {
return nombre;
}
}
C) La enumeración Flag contiene los siguientes valores:
Flag es una enumeración con los siguientes valores:
```java
publicenum Flag {
EXPLICIT("Explicit"),
NSFW("NSFW"),
RELIGION("Religion"),
POLITICAL("Political"),
RACIST("Racist"),
SEXIST("Sexist");
//...}
Detalle de implementación de la enumeración Flag
package com.javhoz.ad.chistes.model;
/**
* Updated by javhoz on 16/01/2025.
* Enumeración de banderas de chistes.
* Pueden ser: NSFW, RELIGION, POLITICAL, RACIST, SEXIST
* Atributo: String nombre.
* Constructor: Flag(String nombre)
* @see Categoria
* @link <a href="https://v2.jokeapi.dev/flags">https://v2.jokeapi.dev/flags</a>
*/publicenum Flag {
EXPLICIT("Explicit"),
NSFW("NSFW"),
RELIGION("Religion"),
POLITICAL("Political"),
RACIST("Racist"),
SEXIST("Sexist");
privatefinal String nombre;
Flag(String nombre) {
this.nombre= nombre;
}
public String getNombre() {
return nombre;
}
/**
* Devuelve la bandera a partir de su nombre.
* @param nombre Nombre de la bandera
* @return Bandera
*/publicstatic Flag getFlag(String nombre) {
// Con expresiones lambda:return java.util.Arrays.stream(Flag.values()).filter(f -> f.getNombre().equals(nombre)).findFirst()
.orElse(null);
/* // Con un bucle for:
// for (Flag f : Flag.values()) {
// if (f.getNombre().equals(nombre)) {
// return f;
// }
// }
// return null;
*/ }
/**
* Sobreescribe el método toString() para que devuelva el nombre de la bandera.
* @return Nombre de la bandera
* @see java.lang.Enum#toString()
*/@Overridepublic String toString() {
return nombre;
}
}
D)Lenguaje es una enumeración con los siguientes valores:
package com.javhoz.ad.chistes.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Updated by javhoz on 16/01/2025.
* <p>
* Clase que representa un chiste.
* Atributos: id de tipo int, categoria de tipo Categoria, idiomade tipo Lenguaje, tipo de TipoChiste,
* List<Flag> banderas, String chiste, String respuesta.
*/publicclassChiste {
privateint id;
private Categoria categoria;
private TipoChiste tipo;
privatefinal List<Flag> banderas;
private String chiste;
private String respuesta;
private Lenguaje lenguaje;
/**
* Constructor de la clase Chiste.
* @param id Identificador del chiste
* @param categoria Categoría del chiste
* @param idioma Idioma del chiste
* @param tipo Tipo del chiste
* @param chiste Chiste
* @param respuesta Respuesta del chiste
*/publicChiste(int id, Categoria categoria, String idioma, TipoChiste tipo, String chiste, String respuesta) {
this.id= id;
this.categoria= categoria;
this.tipo= tipo;
this.chiste= chiste;
this.respuesta= respuesta;
this.banderas=new ArrayList<>();
this.lenguaje= Lenguaje.getLenguaje(idioma);
}
/**
* Constructor por defecto de la clase Chiste.
*
*/publicChiste() {
// this.id = 0;this.categoria= Categoria.ANY;
this.lenguaje= Lenguaje.EN;
this.tipo= TipoChiste.SINGLE;
this.chiste="";
this.respuesta="";
this.banderas=new ArrayList<>();
}
/**
* Devuelve el identificador del chiste.
* @return Identificador del chiste
*/publicintgetId() {
return id;
}
/**
* Establece el identificador del chiste.
* @param id Identificador del chiste
*/publicvoidsetId(int id) {
this.id= id;
}
/**
* Devuelve la categoría del chiste.
* @return Categoría del chiste
*/public Categoria getCategoria() {
return categoria;
}
public String getCategoriaString() {
return categoria.getNombre();
}
/**
* Establece la categoría del chiste.
* @param categoria Categoría del chiste
*/publicvoidsetCategoria(Categoria categoria) {
this.categoria= categoria;
}
publicvoidsetCategoria(String categoria) {
this.categoria= Categoria.getCategoria(categoria);
}
public Lenguaje getLenguaje() {
return lenguaje;
}
public String getLenguajeString() {
return lenguaje.getLenguaje();
}
publicvoidsetLenguaje(String lenguaje) {
this.lenguaje= Lenguaje.getLenguaje(lenguaje);
}
publicvoidsetLenguaje(Lenguaje lenguaje) {
this.lenguaje= lenguaje;
}
/**
* Devuelve el tipo del chiste.
* @return Tipo del chiste
*/public TipoChiste getTipo() {
return tipo;
}
public String getTipoString() {
return tipo.getNombre();
}
/**
* Establece el tipo del chiste.
* @param tipo Tipo del chiste
*/publicvoidsetTipo(TipoChiste tipo) {
this.tipo= tipo;
}
publicvoidsetTipo(String tipo) {
this.tipo= TipoChiste.getTipoChiste(tipo);
}
/**
* Devuelve las banderas del chiste.
* @return Banderas del chiste
*/public List<Flag>getBanderas() {
return banderas;
}
/**
* Añade una bandera al chiste.
* @param flag Bandera a añadir
*/publicvoidaddFlag(Flag flag) {
banderas.add(flag);
}
publicbooleanremoveFlag(Flag bandera) {
return banderas.remove(bandera);
}
/**
* Si el chiste tiene esa bandera, devuelve true.
* @param bandera Bandera a comprobar
* @return true si el chiste tiene esa bandera, false en caso contrario
*/publicbooleancontainsFlag(Flag bandera) {
return banderas.contains(bandera);
}
/**
* Devuelve el chiste como cadena de caracteres.
* @return Chiste como String
*/public String getChiste() {
return chiste;
}
/**
* Establece el chiste.
* @param chiste Chiste
*/publicvoidsetChiste(String chiste) {
this.chiste= chiste;
}
/**
* Devuelve la respuesta del chiste.
* @return Respuesta del chiste
*/public String getRespuesta() {
return respuesta;
}
/**
* Establece la respuesta del chiste.
* @param respuesta Respuesta del chiste
*/publicvoidsetRespuesta(String respuesta) {
this.respuesta= respuesta;
}
@Overridepublicbooleanequals(Object o) {
if (this== o) returntrue;
if (o ==null|| getClass() != o.getClass()) returnfalse;
Chiste chiste = (Chiste) o;
return id == chiste.id;
}
@OverridepublicinthashCode() {
return Objects.hash(id);
}
/**
* Sobrescritura del método toString() para que devuelva el chiste.
* Lo devuelve empleando un StringBuilder y por medio del método forEach() para recorrer la lista de banderas.
* @return Chiste como String
*/@Overridepublic String toString() {
StringBuilder sb =new StringBuilder();
sb.append("Chiste: ").append(chiste).append(System.lineSeparator());
sb.append("Respuesta: ").append(respuesta).append(System.lineSeparator());
sb.append("Categoría: ").append(categoria).append(System.lineSeparator());
sb.append("Idioma: ").append(lenguaje).append(System.lineSeparator());
sb.append("Tipo: ").append(tipo).append(System.lineSeparator());
sb.append("Banderas: ");
banderas.forEach(b -> sb.append(b).append(" "));
sb.append(System.lineSeparator());
return sb.toString();
}
}
B) El adapter ChisteDeserializer:
Detalle de implementación de la clase ChisteDeserializer
package com.javhoz.ad.chistes.model;
import com.google.gson.*;
import java.lang.reflect.Type;
/*
{
"error": false,
"category": "Programming",
"type": "twopart",
"setup": "¿Por qué C consigue todas las chicas y Java no tiene ninguna?",
"delivery": "Porque C no las trata como objetos.",
"flags": {
"nsfw": false,
"religious": false,
"political": false,
"racist": false,
"sexist": false,
"explicit": false
},
"safe": true,
"id": 6,
"lang": "es"
}
*/publicclassChisteDeserializerimplements JsonDeserializer<Chiste> {
@Overridepublic Chiste deserialize(JsonElement elemento, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
// Comprobación si es un objeto:if (!elemento.isJsonObject())
returnnull;
// Creo un chiste vacío, al que le daré valor a sus atributos: Chiste chiste =new Chiste();
// Recupero el objeto JSON del chiste JsonObject jsonChiste = elemento.getAsJsonObject();
// Comprobación de que no hay error en la petición:if (jsonChiste.get("error") !=null&& jsonChiste.get("error").getAsBoolean()) {
returnnull;
}
// Compruebo que cada elemento del objeto existe y lo asigno al objeto Chiste:// La comprobación se hace con el método get() de la clase JsonObject que devuelve// un JsonElement. Si es null, no existe el elemento.if (jsonChiste.get("category") !=null) {
chiste.setCategoria(jsonChiste.get("category").getAsString());
}
if (jsonChiste.get("type") !=null) {
chiste.setTipo(jsonChiste.get("type").getAsString());
}
// En realidad, dependiendo del tipo de chiste, el setup o el delivery pueden no existir.// Por lo que podría hacer comprobando el valor de type, pero lo dejo así para que veáis// como se puede hacer con el método get() de la clase JsonObject.if (jsonChiste.get("setup") !=null) {
chiste.setChiste(jsonChiste.get("setup").getAsString());
if (jsonChiste.get("delivery") !=null) {
chiste.setRespuesta(jsonChiste.get("delivery").getAsString());
}
} elseif (jsonChiste.get("joke") !=null) {
chiste.setChiste(jsonChiste.get("joke").getAsString());
}
if (jsonChiste.get("lang") !=null) {
chiste.setLenguaje(jsonChiste.get("lang").getAsString());
}
if (jsonChiste.get("id") !=null) {
chiste.setId(jsonChiste.get("id").getAsInt());
}
if (jsonChiste.get("flags") !=null) {
JsonObject flags = jsonChiste.get("flags").getAsJsonObject();
if (flags.get("nsfw").getAsBoolean()) {
chiste.addFlag(Flag.NSFW);
}
if (flags.get("religious").getAsBoolean()) {
chiste.addFlag(Flag.RELIGION);
}
if (flags.get("political").getAsBoolean()) {
chiste.addFlag(Flag.POLITICAL);
}
if (flags.get("racist").getAsBoolean()) {
chiste.addFlag(Flag.RACIST);
}
if (flags.get("sexist").getAsBoolean()) {
chiste.addFlag(Flag.SEXIST);
}
if (flags.get("explicit").getAsBoolean()) {
chiste.addFlag(Flag.EXPLICIT);
}
}
return chiste;
}
}
C) La clase ChisteTypeAdapter:
Detalle de implementación de la clase ChisteTypeAdapter
package com.javhoz.ad.chistes.model;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
/*
Formato de JSON:
{
"id": 1,
"category": "Programming",
"type": "single",
"joke": "Chuck Norris can write multithreaded applications with a single thread.",
"flags": {
"nsfw": false,
"religious": false,
"political": false,
"racist": false,
"sexist": false
},
"lang": "en"
*//**
* Updated by javhoz on 16/01/2025.
* Clase que adaptará el tipo Chiste para que pueda ser serializado y deserializado por Gson.
*
* @see com.google.gson.Gson
* @see com.google.gson.TypeAdapter
* @see com.google.gson.GsonBuilder
* @see com.google.gson.JsonDeserializer
*/publicclassChisteTypeAdapterextends TypeAdapter<Chiste> {
@Overridepublicvoidwrite(JsonWriter jsonWriter, Chiste chiste) throws IOException {
jsonWriter.beginObject();
jsonWriter.name("id").value(chiste.getId());
jsonWriter.name("category").value(chiste.getCategoriaString());
jsonWriter.name("type").value(chiste.getTipoString());
if (chiste.getTipo() == TipoChiste.SINGLE) {
jsonWriter.name("joke").value(chiste.getChiste());
} else {
jsonWriter.name("setup").value(chiste.getChiste());
jsonWriter.name("delivery").value(chiste.getRespuesta());
}
jsonWriter.name("flags");
jsonWriter.beginObject();
// Recorremos todas las banderas y asignamos el valor verdadero o falso si el chiste la contiene o no, respectivamente.// Puede hacerse por medio del método containsFlag() de la clase Chiste o recoger las banderas// del chiste e invocar el método contains() de la clase List.for (Flag flag : Flag.values()) {
jsonWriter.name(flag.getNombre().toLowerCase()).value(chiste.containsFlag(flag));
}
jsonWriter.endObject();
jsonWriter.name("lang").value(chiste.getLenguajeString());
jsonWriter.endObject();
}
/**
* Método que deserializa un objeto Chiste a partir de un JsonReader.
*
* @param reader JsonReader que contiene el objeto Chiste
* @return Objeto Chiste
* @throws IOException Si hay un error de E/S
* @see com.google.gson.stream.JsonReader
* @see com.google.gson.stream.JsonToken
*/@Overridepublic Chiste read(JsonReader reader) throws IOException {
if(reader.peek()== JsonToken.NULL|| reader.peek()!= JsonToken.BEGIN_OBJECT){
// reader.nextNull();returnnull;
}
reader.beginObject();
Chiste chiste =new Chiste();
while (reader.peek() != JsonToken.END_OBJECT) {
String name = reader.nextName();
switch (name) {
case"id"-> chiste.setId(reader.nextInt());
case"category"-> chiste.setCategoria(Categoria.getCategoria(reader.nextString()));
case"type"-> chiste.setTipo(TipoChiste.getTipoChiste(reader.nextString()));
case"joke", "setup"-> chiste.setChiste(reader.nextString());
case"delivery"-> chiste.setRespuesta(reader.nextString());
case"flags"->// Para hacerlo más modular he puesto el código en un método aparte. readFlags(reader, chiste);
case"lang"-> chiste.setLenguaje(reader.nextString());
default-> reader.skipValue();
}
}
reader.endObject();
return chiste;
}
privatevoidreadFlags(JsonReader reader, Chiste chiste) throws IOException {
reader.beginObject();
while (reader.peek() != JsonToken.END_OBJECT) {
String flagName = reader.nextName();
switch (flagName) {
case"nsfw"-> {
if (reader.nextBoolean()) chiste.addFlag(Flag.NSFW);
}
case"religious"-> {
if (reader.nextBoolean()) chiste.addFlag(Flag.RELIGION);
}
case"political"-> {
if (reader.nextBoolean())
chiste.addFlag(Flag.POLITICAL);
}
case"racist"-> {
if (reader.nextBoolean())
chiste.addFlag(Flag.RACIST);
}
case"sexist"-> {
if (reader.nextBoolean())
chiste.addFlag(Flag.SEXIST);
}
case"explicit"-> {
if (reader.nextBoolean())
chiste.addFlag(Flag.EXPLICIT);
}
default-> reader.skipValue();
}
}
reader.endObject();
}
}
D) La interface IChisteDAO y clase ChisteDAO se usa para obtener los chistes de la API:
Detalle de implementación de la interfaz IChisteDAO
Podrías realizar mejoras en el código, como la gestión de excepciones, la comprobación de valores nulos, la simplificación de código, etc.
Detalle de implementación de la clase ChisteDAO
package com.javhoz.ad.chistes.model;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Objects;
/**
* Created by Pepe Calo on 07/11/2023
* Implementación de la interfaz IChisteDAO que consulta un chiste en un archivo Json
* mediante la librería Gson.
* La API de chistes utilizada es:
* <a href="https://v2.jokeapi.dev/joke/">...</a>
*
* @see IChisteDAO
* @see Chiste
* @see Gson
* @see GsonBuilder
* @see com.google.gson.JsonObject
* @see com.google.gson.JsonParser
*/publicclassChisteDAOimplements IChisteDAO {
privatefinal Gson gson;
// https://v2.jokeapi.dev/joke/Programming,Miscellaneous?blacklistFlags=nsfw,religiousprivatestaticfinal String BASE_URL ="https://v2.jokeapi.dev/joke/";
privatestaticfinal String ENDPOINT ="?format=json";
privatestaticfinalint NO_ID = 0;
privatestaticfinal String SINGLE ="single";
/**
* Constructor de la clase ChisteDAO.
* Si deseas emplear las clases ChisteSerializer y ChisteDeserializer, debes comentar la línea con ChisteTypeAdapter
* y no comentar las de los otros dos adaptadores.
*/publicChisteDAO() {
gson =new GsonBuilder().setPrettyPrinting()
// .registerTypeAdapter(Chiste.class, new ChisteDeserializer())// .registerTypeAdapter(Chiste.class, new ChisteSerializer()) .registerTypeAdapter(Chiste.class, new ChisteTypeAdapter())
.create();
}
private String getURL(String categoria, String[] tipo, String[] banderas, String idioma, int id) {
String url = BASE_URL + categoria + ENDPOINT;
if (tipo !=null&& tipo.length> 0) {
// Concateno los elementos no nulos media stream de un array de String. En el caso de que no haya ninguno, devuelvo un Optional vacío. String tipos = Arrays.stream(tipo).filter(Objects::nonNull).reduce((s, s2) -> s +","+ s2).orElse(null);
if(tipos!=null&&!tipos.isEmpty()){
url +="&type="+ tipos;
}
}
if (banderas !=null&& banderas.length> 0) {
String flags = Arrays.stream(banderas).filter(Objects::nonNull).reduce((s, s2) -> s +","+ s2).orElse(null);
if(flags!=null&&!flags.isEmpty()){
url +="&blacklistFlags="+ flags;
}
}
if (idioma !=null&&!idioma.isEmpty()) {
url +="&lang="+ idioma;
}
if (id > 0) {
url +="&idRange="+ id;
}
System.out.println("url = "+ url);
return url;
}
private Chiste getJoke(String url) {
try (BufferedReader is =new BufferedReader(new InputStreamReader(new URI(url).toURL().openStream()))) {
return gson.fromJson(is, Chiste.class);
} catch (MalformedURLException e) {
System.err.println("Error en la URL: "+ e.getMessage());
} catch (IOException e) {
System.err.println("Erro E/S: "+ e.getMessage());
} catch (URISyntaxException e) {
thrownew RuntimeException(e);
}
returnnull;
}
private String getJokeAsString(String url) {
Chiste chiste = getJoke(url);
return (chiste!=null) ? chiste.getChiste() + System.lineSeparator() + chiste.getRespuesta() : "";
}
@Overridepublic String getJokeAsString(String categoria, String[] tipo, String[] banderas) {
return getJokeAsString(getURL(categoria, tipo, banderas, null, NO_ID));
}
@Overridepublic Chiste getJoke(String categoria, String[] tipo, String[] banderas) {
return getJoke(getURL(categoria, tipo, banderas, null, NO_ID));
}
@Overridepublic String getJokeAsString(String categoria, String[] tipo, String[] banderas, String idioma) {
return getJokeAsString(getURL(categoria, tipo, banderas, idioma, NO_ID));
}
@Overridepublic Chiste getJoke(String categoria, String[] tipo, String[] banderas, String idioma) {
return getJoke(getURL(categoria, tipo, banderas, idioma, NO_ID));
}
@Overridepublic Chiste getJokeById(int id) {
return getJoke(getURL("Any", null, null, null, id));
}
@OverridepublicvoidsaveJokeAsJson(Chiste chiste, Writer writer) {
gson.toJson(chiste, writer);
}
@Overridepublic String getRandomJokeAsString() {
System.out.println(BASE_URL +"Any");
return getJokeAsString(BASE_URL +"Any");
}
@Overridepublic Chiste getRandomJoke() {
return getJoke(BASE_URL +"Any");
}
}
Ejercicio
Crear una base de datos con JPA y Hibernate para la aplicación JokeAPI y transfiere todos los datos de JSON a la base de datos.
Añade las dependencias necesarias y el fichero de configuración persistence.xml en el directorio META-INF de src/main/resources:
<?xml version="1.0" encoding="UTF-8"?><persistencexmlns="https://jakarta.ee/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"version="3.0"><persistence-unitname="chistesH2"transaction-type="RESOURCE_LOCAL"><provider>org.hibernate.jpa.HibernatePersistenceProvider</provider><!-- <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>--><exclude-unlisted-classes>false</exclude-unlisted-classes><properties><propertyname="jakarta.persistence.jdbc.url"value="jdbc:h2:RutaABaseDatos;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO"/><propertyname="jakarta.persistence.jdbc.user"value=""/><propertyname="jakarta.persistence.jdbc.password"value=""/><propertyname="jakarta.persistence.jdbc.driver"value="org.h2.Driver"/><!-- Automáticamente, genera el esquema de la base de datos --><propertyname="jakarta.persistence.schema-generation.database.action"value="drop-and-create"/><!-- Muestra por pantalla las sentencias SQL --><propertyname="hibernate.show_sql"value="false"/><propertyname="hibernate.format_sql"value="true"/><propertyname="hibernate.highlight_sql"value="true"/><propertyname="hibernate.dialect"value="org.hibernate.dialect.H2Dialect"/></properties></persistence-unit></persistence>
Para ello, crea las siguientes clases:
A) ChisteJpaManager que empleando el patrón Singleton, gestione la creación de la factoría de entidades y el EntityManager.
B) Chiste que emplea JPA para mapear la clase Chiste con la tabla Chiste de la base de datos.
Solución de Chiste
package com.javhoz.ad.chistes.model;
import jakarta.persistence.*;
import java.util.ArrayList;
@EntitypublicclassChisteimplements java.io.Serializable {
@Id@Column(name ="idChiste")
privateint id;
private Categoria categoria;
private TipoChiste tipo;
// Como se trata de una relación muchos a muchos, se emplea la anotación @ElementCollection// H2 admite el tipo de dato Array de enteros (TINYINT ARRAY), prueba a no poner la anotación @ElementCollection ni @CollectionTable@ElementCollection// Para que se cree una tabla intermedia@Enumerated(EnumType.STRING)
@CollectionTable(name ="FlagsChiste", joinColumns =@JoinColumn(name ="idChiste"))
privatefinal List<Flag> banderas;
private String chiste;
private String respuesta;
private Lenguaje lenguaje;
//...}
C)ChisteDownloader que descarga los chistes de la API y los guarda en la base de datos.
Ten el cuenta que ChisteDownloader es un Singleton y que se puede configurar el número de chistes a descargar, además de un tiempo de espera entre chiste y chiste (la API sólo permite 120 peticiones por minuto).
Por ello, haz que sea un hilo que se ejecute cada cierto tiempo ( implements Runnable ) y tenga los siguientes atributos:
tiempoEspera que es el tiempo de espera entre chiste y chiste.
instance que es la instancia de ChisteDownloader.
MAX_CHISTES que es el número máximo de chistes a descargar.
chisteDAO que es el DAO de Chiste.
numeroChistes que es el número de chistes a descargar (si no se indica debe ser MAX_CHISTES).
El componente de mapeo objeto-relacional (ORM) incluye:
Correspondencia del estado del objeto con las columnas de la base de datos.
Cómo enviar consultas entre los objetos.
En este apartado veremos cómo mapear entidades y atributos con la base da datos y generar automáticamente identificadores de entidad.
2. Anotaciones de Persistencia
Las especificaciones de Jakarta Persistence (y de Enterprise Beans) emplea principalmente anotaciones.
Las anotaciones pueden aplicarse a clases, métodos y atributos.
La anotación debe colocarse principio a la definición de código del artefacto que se está anotando: bien en la misma línea justo antes de la clase, método o atributo o en la línea superior.
Consejo
La elección se basa completamente en las preferencias de la persona que aplica las anotaciones, y creo que tiene sentido hacer una cosa en algunos casos y la otra en otros casos. Depende de cuán extensa sea la anotación y cuál sea el formato más legible.
Las anotaciones de Jakarta Persistence fueron diseñadas para ser legibles, fáciles de especificar y lo suficientemente flexibles como para permitir diferentes combinaciones de metadatos. La mayoría de las anotaciones se especifican como hermanas en lugar de estar anidadas entre sí, lo que significa que múltiples anotaciones pueden anotar la misma clase, atributo o propiedad en lugar de tener anotaciones incrustadas dentro de otras anotaciones.
Las anotaciones de mapeo se pueden clasificar en dos categorías:
Anotaciones lógicas: describen el modelo de entidad desde una perspectiva de modelado de objetos. Están fuertemente vinculadas al modelo de dominio y son el tipo de metadatos que podría querer especificar en UML o cualquier otro lenguaje o marco de modelado de objetos Ejemplos: @Entity, @Id, @ManyToOne, @OneToMany, @ManyToMany, @OneToOne.
Anotaciones físicas: se relacionan con el modelo de datos concreto de la base de datos. Tratan con tablas, columnas, restricciones y otros artefactos en base de datos de los que el modelo de objetos podría no estar al tanto de otra manera. Ejemplos: @Table, @Column, @JoinColumn, @JoinTable.
Existen equivalentes XML para todas las anotaciones de mapeo lo que permite utilizar el enfoque que mejor se adapte a las necesidades de desarrollo. Nosotros nos centraremos en anotaciones, que es la forma más común de especificar metadatos en aplicaciones modernas.
Nota
Consejo: Las anotaciones de mapeo de JPA se pueden aplicar a atributos o métodos. Si se aplican a un atributo, el proveedor de persistencia accederá al atributo directamente. Si se aplican a un método, el proveedor de persistencia accederá al atributo a través del método getter y setter. Lo veremos ahora.
1. Modo de acceso a una Entidad
La forma en que se accede al estado en la entidad desde el proveedor de persistencia se llama modo de acceso.
El mecanismo que se usa para designar el estado persistente es el mismo que el modo de acceso que el proveedor utiliza para acceder a ese estado, y hay dos modos de acceso: acceso por atributo (atributos de la entidad) y acceso por propiedad (métodos getter y setter de la entidad).
Acceso por atributo: a partir de los atributos/atributos de la entidad utilizando reflexión (Java reflection) (se precisa un @Id sobre el atributo).
Acceso por propiedad: las anotaciones se colocan en los métodos getter de las propiedades, esos métodos getter y setter serán invocados por el proveedor para acceder y establecer el estado. En este caso se indica la anotación@Id en el método getter.
1.2. Acceso por atributo
Anotar los atributos de la entidad hará que el proveedor use el acceso por atributo para obtener y establecer el estado de la entidad. Los métodos getter y setter pueden estar presentes o no, pero si están presentes, el proveedor los ignora.
Todos los atributos deben declararse como protected, de paquete (sin modificador) o private. Se prohíben los atributos public.
El ejemplo de entidad Employee mapeada usando el acceso por atributo:
La anotación @Id indica que id es el identificador persistente o clave primaria de la entidad y que se debe asumir el acceso por atributo.
Los atributos name y salary se configuran por defecto como persistentes y se mapean a columnas del mismo nombre.
Cuando se utiliza el modo de acceso por propiedad debe haber métodos getter y setter para las propiedades persistentes.
El tipo de propiedad se determina por el tipo devuelto del método getter y debe ser el mismo que el tipo del único parámetro pasado al método setter.
Ambos métodos deben tener visibilidad public o protected.
Las anotaciones de mapeo para una propiedad deben estar en el método getter.
Ejemplo de la clase Employee tiene una anotación @Id en el método getId(), por lo que el proveedor utilizará el acceso por propiedad para obtener y establecer el estado de la entidad. Las propiedades name y salary se harán persistentes gracias a los métodos getter y setter y se mapearán a las columnas NAME y SALARY, respectivamente.
Observa que la propiedad salary está respaldada por el atributo wage, que no comparte el mismo nombre. Esto pasa desapercibido para el proveedor porque al especificar el acceso por propiedad, le estamos diciendo al proveedor que ignore los atributos de la entidad y utilice solo los métodos getter y setter para la nomenclatura.
Por lo general se accede a los datos a través del acceso por atributo, pero posible combinar el acceso por atributo con el acceso por propiedad dentro de la misma jerarquía de entidades o dentro de la misma entidad. Puede ser útil cuando se agrega una subclase de entidad a una jerarquía existente que utiliza un tipo de acceso diferente.
Agregar una anotación @Access con un modo de acceso hace que el tipo de acceso predeterminado se anule para esa subclase de entidad.
@Accesstambién es útil cuando es necesario realizar una simple transformación de los datos al leer o escribir en la base de datos.
Por ejemplo, la entidad Employee que tiene un modo de acceso predeterminado de AccessType.FIELD, pero la columna de la base de datos almacena el código de área como parte del número de teléfono, y solo queremos almacenar el código de área en el atributo phoneNum de la entidad si no es un número local. Podemos agregar una propiedad persistente que realice la transformación correspondiente en lecturas y escrituras.
Se debe hacer es marcar explícitamente el modo de acceso predeterminado para la clase mediante la anotación @Access e indicar el tipo de acceso. A menos que se haga esto, será indefinido si ambos atributos y propiedades están anotados:
Se anota el atributo o propiedad adicional con la anotación @Access, pero esta vez especificando el tipo de acceso opuesto al especificado a nivel de clase. No es redundante especificar el tipo de acceso de AccessType.PROPERTY en una propiedad persistente porque es obvio al verlo que es una propiedad, pero al hacerlo se indica que es una excepción al caso predeterminado:
El atributo o propiedad correspondiente al que se está haciendo persistente debe marcarse como *transient* para que las reglas de acceso predeterminadas no provoquen que el mismo estado se persista dos veces. El atributo en el cual se está almacenando el estado de la propiedad persistente en la entidad debe estar anotado con @Transient:
@Transientprivate String phoneNum; // no persiste este atributo, pues se persiste el atributo por la propiedad getPhoneNumberForDb()
Ejemplo completo de la clase Employee con un atributo phoneNum que se mapea a la columna PHONE de la base de datos, pero que realiza una transformación simple en la lectura y escritura:
@Entity@Access(AccessType.FIELD)
publicclassEmployee {
publicstaticfinal String LOCAL_AREA_CODE ="613";
@Idprivatelong id;
@Transientprivate String phoneNum;
// ...publiclonggetId() { return id; }
publicvoidsetId(long id) { this.id= id; }
public String getPhoneNumber() { return phoneNum; }
publicvoidsetPhoneNumber(String num) {
this.phoneNum= num;
}
@Access(AccessType.PROPERTY)
@Column(name="PHONE") // Si no se indica la columna, se mapearía a PhoneNumberForDbprotected String getPhoneNumberForDb() {
if (phoneNum.length() == 10)
return phoneNum;
elsereturn LOCAL_AREA_CODE + phoneNum;
}
protectedvoidsetPhoneNumberForDb(String num) {
if (num.startsWith(LOCAL_AREA_CODE))
phoneNum = num.substring(3);
else phoneNum = num;
}
// ...}
Ejercicio 05.01. Acceso combinado a la entidad Chiste.
Mofifica la entidad Chiste para que guarde el chiste y la respuesta en un solo campo en la base de datos, pero que se muestren por separado en la aplicación.
Para mapear entidad a una tabla de la base de datos una entidad sólo se necesitan las anotaciones @Entity y @Id.
El nombre de la tabla predeterminado es el nombre no calificado de la clase de entidad.
Para cambiar el nombre predeterminado de la tabla se anota la clase de entidad con la anotación @Table e incluyendo el nombre de la tabla mediante el elemento name. Por ejemplo:
Consejo Los nombres predeterminados no se especifican en mayúsculas o minúsculas. La mayoría de las bases de datos no distinguen entre mayúsculas y minúsculas, por lo que generalmente no importará si un proveedor usa el caso del nombre de la entidad o lo convierte a mayúsculas.
"_In MySQL text columns are case insensitive by default, while in H2 they are case
sensitive. However H2 supports case insensitive columns as well. To create the tables
with case insensitive texts, append IGNORECASE=TRUE to the database URL (example:
jdbc:h2:~/test;IGNORECASE=TRUE)."_
Esquemas:
La anotación @Table proporciona la capacidad también de especificar un esquema o catálogo de la base de datos. El nombre del esquema se usa comúnmente para diferenciar un conjunto de tablas de otro y se indica mediante el uso del elemento schema. Por ejemplo: la entidad Employee que se asigna a la tabla EMP en el esquema HR.
El nombre del esquema se antepondrá al nombre de la tabla cuando el proveedor de persistencia vaya a la base de datos para acceder a la tabla (HR.EMP en el ejemplo).
Consejo
Algunos proveedores pueden permitir que el esquema se incluya en el elemento name de la tabla sin tener que especificar el elemento schema, como en @Table(name="HR.EMP"), pero no es un estándar.
Catálogos:
Algunas bases de datos admiten la noción de un catálogo. Para estas bases de datos, se puede especificar el elemento catalog de la anotación @Table. Por ejemplo, para la tabla EMP.
2.1. Nombres sensibles a mayúsculas de tablas y columnas
Los nombres de las tablas y columnas como identificadores en mayúsculas ayudan a diferenciarlos de los identificadores en Java y es el estándar SQL establece que los identificadores de base de datos no delimitados no sean sensibles a mayúsculas y la mayoría tiende a mostrarlos en mayúsculas.
@Table y @Column la cadena de identificador se pasa al controlador JDBC exactamente como se especifica o establece por defecto. Por ejemplo, cuando no se especifica un nombre de tabla para la entidad Autor, entonces el nombre de la tabla asumido y utilizado por el proveedor será Autor, que por definición SQL no es diferente de AUTOR.
Las siguientes anotaciones deberían ser equivalentes, ya que se refieren a la misma tabla en una base de datos compatible con el estándar SQL:
Aunque no es común ni una buena práctica, en teoría, una base de datos podría tener una tabla AUTOR y otra Autor.
Estas necesitarían ser envueltas en comillas dobles para distinguirlas, que deben ser escapadas, alrededor del identificador. El mecanismo de escape es la barra invertida (el carácter ):
@Table(name="\"Autor\"")
@Table(name="\"AUTOR\"") // Son tablas diferentes
3. Mapeo de Tipos Simples
Los tipos simples de Java se asignan de manera inmediata en campos o propiedades de una entidad. Incluyen:
Tipos primitivos de Java: byte, int, short, long, boolean, char, float y double.
Clases envolventes de tipos primitivos de Java: Byte, Integer, Short, Long, Boolean, Character, Float y Double
Tipos de arrays de byte y carácter: byte[], Byte[], char[] y Character[]
Tipos numéricos grandes: java.math.BigInteger y java.math.BigDecimal
Cadenas: java.lang.String
Tipos temporales de Java: java.util.Date y java.util.Calendar, además de todos los subtipos y las Java 8 java.time API:
java.time.LocalDate
java.time.LocalTime
java.time.LocalDateTime
java.time.OffsetTime
java.time.OffsetDateTime
Para tipos como java.time.Instant, se necesita un AttributeConverter, que veremos más adelante.
Tipos temporales JDBC: java.sql.Date, java.sql.Time y java.sql.Timestamp
Tipos enumerados: cualquier tipo enumerado definido por el sistema o el usuario
Objetos serializables: cualquier tipo serializable definido por el sistema o el usuario.
Si el tipo de la capa JDBC no se puede convertir al tipo de Java del campo o propiedad, normalmente se lanzará una excepción, aunque no está garantizado.
Consejo
Cuando el tipo persistente no coincide con el tipo JDBC, algunos proveedores pueden optar por tomar medidas propietarias o hacer una suposición para convertir entre los dos. En otros casos, el controlador JDBC podría realizar la conversión por sí mismo.
Opcionalmente, se puede colocar una anotación @Basic en un campo o propiedad para marcarlo explícitamente como persistente. Esta anotación es principalmente con fines de documentación y no es necesaria para que el campo o propiedad sea persistente.
4. Mapeo de columnas: @Column
La anotación @Basic (o el mapeo básico asumido en su ausencia) puede considerarse como una indicación lógica de que un atributo dado es persistente.
La anotación física que acompaña al mapeo básico es la anotación @Column:
Con @Column en el atributo indica características específicas de la columna física de la base de datos. El nombre de la columna y los metadatos de asignación física pueden estar en un archivo XML separado.
Elementos de la anotación @Column:
name: nombre de la columna de la base de datos. String predeterminado es el nombre del atributo o propiedad.
length: longitud de la columna de la base de datos. Solo se aplica si el tipo de columna es una cadena o un tipo de array de caracteres. Por defecto 255.
unique: si es una clave primaria (valor único). Booleano con un valor predeterminado de false.
nullable: si puede ser nulo. Booleano con un valor predeterminado de true.
insertable: si el valor de la columna se incluye en las declaraciones de SQL INSERT generadas. Booleano con un valor predeterminado de true.
updatable: si el valor de la columna se incluye en las declaraciones de SQL UPDATE generadas por el proveedor. Este es un atributo booleano con un valor predeterminado de true.
precision y scale: se aplican a los tipos numéricos y se utilizan para especificar la precisión y la escala de la columna de la base de datos. Si se omite, se utilizarán los valores predeterminados 0.
table: El nombre de la tabla de la base de datos que contiene la columna. Este nombre se refiere a la tabla que contiene la columna, que puede ser la tabla de la entidad o una tabla secundaria. Esta anotación es útil para especificar una columna que se mapea a una tabla secundaria.
columnDefinition: definición de columna SQL, que es una cadena que se pasará directamente al DDL de la base de datos. Esta característica puede hacer que la aplicación sea menos portátil. Si se omite, se utilizará la definición de columna predeterminada del proveedor de persistencia.
El principal elemento que es relevante es el elemento name, que es simplemente una cadena que especifica el nombre de la columna a la que se ha asignado el atributo.
A veces, alguna parte de la entidad se accede pocas veces (imagen, etc.). En estas situaciones, se puede optimizar el rendimiento al recuperar sólo los datos que se espera que se accedan con frecuencia. Es lo que se denomina: carga perezosa, carga diferida, carga lenta, carga bajo pedido, lectura justo a tiempo, indirección y otros.
En este caso, los datos del objeto no se leen inicialmente desde la base de datos, sino que se recuperarán solo cuando se hagan referencia o se lean.
Se especifica con el elemento fetch de la anotación @Basic, que se corresponde con un valor de la enumeración FetchType:
EAGER (por defecto): carga ansiosa.
LAZY: el proveedor puede posponer la carga del estado para ese atributo hasta que se haga referencia.
Casi nunca es una buena idea cargar perezosamente los tipos simples. Las únicas veces en las que debería considerarse la carga perezosa de un mapeo básico son cuando hay muchas columnas en una tabla (por ejemplo, docenas o cientos) o cuando las columnas son grandes (por ejemplo, cadenas de caracteres o cadenas de bytes muy grandes).
La aplicación no tiene que hacer nada especial para obtenerlo. Al acceder al campo de comentarios, se leerá y completará automáticamente por el proveedor si aún no se había cargado.
Consejo
La directiva LAZY solo pretende ser una sugerencia para el proveedor de persistencia para ayudar a la aplicación a lograr un mejor rendimiento. No se requiere que el proveedor respete la solicitud porque el comportamiento de la entidad no se ve comprometido si el proveedor procede y carga el atributo. La situación contraria no es cierta, ya que especificar que un atributo se cargue ansiosamente podría ser fundamental para poder acceder al estado de la entidad una vez que la entidad se ha desvinculado del contexto de persistencia.
6. Objetos Grandes (LOBs): @Lob
Un LOB es un campo de caracteres o bytes que puede ser muy grande (hasta el rango de gigabytes). Típicamente, CLOB se utiliza para almacenar texto y BLOB para almacenar datos binarios. Los LOB se almacenan en la base de datos, pero se accede a ellos de manera diferente a los tipos simples.
La anotación @Lob se puede usar para los LOB y puede aparecer junto con la anotación @Basic, o puede aparecer cuando @Basic está ausente y se asume implícitamente en el mapeo.
Dado que la anotación @Lob realmente sólo califica el mapeo básico, también puede ir acompañada de una anotación @Column.
Existen (básicamente) dos tipos de LOB en las BD:
CLOB contiene una secuencia de caracteres grande. Los tipos de datos Java son char[], Character[] y objetos String.
BLOB puede almacenar una secuencia de bytes grande. Los tipos de Java asignados a columnas BLOB son byte[], Byte[] y tipos Serializable.
Un ejemplo, en el que se marca LAZY, algo útil en los LOB poco empleados:
Ejercicio 05.02. CLOB y BLOB de una entidad Documento
Crea una entidad Documento que tenga un campo de texto grande (CLOB) para el contenido del documento y un campo de bytes grande (BLOB) para la imagen del documento. Haz pruebas con tres gestores de bases de datos: H2, SQLite y PostgreSQL y comprueba el resultado creando la tabla en cada uno de ellos, con y sin declaración de tipo de LOB.
Consejo: Los LOB son útiles para almacenar datos grandes, pero no se deben abusar de ellos. Los LOB pueden ser ineficientes para recuperar y almacenar. Siempre que sea posible, se deben evitar los LOB. Si se necesita almacenar datos grandes, se debe considerar el uso de un sistema de archivos o un sistema de almacenamiento de objetos.
7. Tipos Enumerados (enum): @Enumerated
Los valores de un tipo enumerado en Java tienen una asignación ordinal implícita que se determina por el orden en que se declararon.
El ordinal se usa de modo predeterminado para representar y almacenar los valores del tipo enumerado en la base de datos.
El proveedor asumirá que la columna de la base de datos es de tipo entero.
EmployeeType, en ejemplo anterior, el atributo type se asignará a una columna TYPE de tipo entero.
Si se cambia el tipo (el orden) hay una inconsistencia y problemas.
En este ejemplo, si la política de beneficios de la empresa cambia y comenzamos a dar beneficios adicionales a los empleados a tiempo parcial que trabajan más de 20 horas por semana, querríamos diferenciar entre los dos tipos de empleados a tiempo parcial. Al agregar un valor PART_TIME_BENEFITS_EMPLOYEE después de PART_TIME_EMPLOYEE, estaríamos provocando una nueva asignación de ordinal, donde nuestro nuevo valor recibiría el ordinal 2 y CONTRACT_EMPLOYEE obtendría 3. Esto tendría el efecto de hacer que todos los empleados contratados previamente como empleados a tiempo parcial se conviertan repentinamente en empleados a tiempo parcial con beneficios, claramente no el resultado que esperábamos.
Una solución es almacenar el nombre de la enumeración como una cadena en lugar de almacenar el ordinal. Para ello existe la anotación @Enumerated:
Para modificar cómo guardar los enumerados se puede realizar con la anotación @Enumerated en el atributo y especificando un valor de EnumType.STRING (la otra posibilidad es EnumType.ORDINAL):
La anotación @Enumerated permite especificar un EnumType, que a su vez es un tipo enumerado que define el valor de value de EnumType.ORDINAL y EnumType.STRING.
El valor predeterminado de @Enumerated es ORDINAL, especificar @Enumerated(ORDINAL) solo es útil cuando se desea hacer explícito este mapeo.
El uso de cadenas resuelve el problema de insertar valores adicionales en medio del tipo enumerado, pero dejará los datos vulnerables a cambios en los nombres de los valores.
Por ejemplo, si quisiéramos cambiar PART_TIME_EMPLOYEE a PT_EMPLOYEE, tendríamos problemas. Aunque este es un problema menos probable, cambiar los nombres de un tipo enumerado obligaría a cambiar todo el código que utiliza ese tipo enumerado, lo cual sería más engorroso que reasignar valores en una columna de base de datos.
Almacenar el ordinal es la mejor y más eficiente manera de manejar los tipos enumerados, siempre y cuando la probabilidad de agregar nuevos valores en el medio no sea alta. Se podrían agregar nuevos valores al final del tipo sin consecuencias negativas.
Consejo
Es posible tener valores enumerados que contengan estado. Actualmente, no hay soporte en Jakarta Persistence para mapear el estado contenido dentro de los valores enumerados, pero hay alguna estrategia que veremos más adelante o en ejercicios.
7.2. Mapeo de enumeraciones con @PostLoad y @PrePersist
Otra opción para la persistencia de enumeraciones es utilizar los métodos del estándar de JPA. Podemos mapear enumeraciones de ida (preescritrua) y vuelta (después de la carga) en los eventos @PostLoad y @PrePersist:
@PostLoad: se invoca después de que se cargue una entidad de la base de datos. PostLoad.
@PrePersist: se invoca antes de que se persista una entidad en la base de datos. PrePersist.
La idea es tener dos atributos en la entidad:
El primero se mapea a un valor de base de datos.
El segundo es un campo @Transient que almacena un valor real de la enumeración, que es utilizado por el código de lógica de negocio.
INSERTINTO Articulo (valorPrioridad, estado, titulo, tipo, id)
VALUES (?, ?, ?, ?, ?)
binding parameter [1] as [INTEGER] - [300]
binding parameter [2] as [INTEGER] - [null]
binding parameter [3] as [VARCHAR] - [Título ejemplo]
binding parameter [4] as [VARCHAR] - [null]
binding parameter [5] as [INTEGER] - [3]
No es ideal tener dos atributos que representan una sola enumeración de la entidad. Además, si usamos este tipo de mapeo, no podemos utilizar el valor del enum en consultas JPQL.
7.3 Mapeo de enumeraciones con @Converter
La versión 2.1 de JPA introdujo una nueva API estandarizada que puede ser utilizada para convertir un atributo de entidad a un valor de base de datos y viceversa. Todo lo que necesitamos hacer es crear una nueva clase que implemente jakarta.persistence.AttributeConverter y anotarla con @Converter.
Una tercera opción es utilizar un @Converter. Un @Converter es una clase que implementa la interfaz AttributeConverter<X, Y>, donde X es el tipo de atributo de la entidad y Y es el tipo de columna de la base de datos. La interfaz AttributeConverter tiene dos métodos:
Y convertToDatabaseColumn(X attribute): convierte el atributo de la entidad en un tipo de columna de la base de datos.
X convertToEntityAttribute(Y dbData): convierte el tipo de columna de la base de datos en un atributo de la entidad.
Class<X> getJavaType(): devuelve el tipo de atributo de la entidad.
Class<Y> getDatabaseType(): devuelve el tipo de columna de la base de datos.
@Converter(autoApply = true): indica que el convertidor debe aplicarse a todos los atributos de la entidad que tengan el tipo de atributo X y el tipo de columna de la base de datos Y.
Hemos configurado el valor autoApply de @Converter en true para que JPA aplique automáticamente la lógica de conversión a todos los atributos mapeados de tipo Categoria. De lo contrario, tendríamos que poner la anotación @Convert directamente en el campo de la entidad.
La anotación @Convert se puede utilizar para aplicar un convertidor a un atributo específico de una entidad. Tiene como parámetros attributeName, converter y disableConversion.
attributeName: nombre del atributo de la entidad a la que será aplicado el convertidor
converter: clase del convertidor.
disableConversion: booleano que indica si se debe deshabilitar la conversión automática (por defecto false). Si está como true, no se aplicará el converter, y no debería estar indicado.
Entonces JPA ejecutará la siguiente instrucción SQL:
insertinto Articulo
(categoria, valorPrioridad, estado, titulo, tipo, id)
values (?, ?, ?, ?, ?, ?)
Valor convertido al enlazar : MUSICA -> M
binding parameter [1] as [VARCHAR] - [M]
binding parameter [2] as [INTEGER] - [0]
binding parameter [3] as [INTEGER] - [null]
binding parameter [4] as [VARCHAR] - [título convertido]
binding parameter [5] as [VARCHAR] - [null]
binding parameter [6] as [INTEGER] - [4]
Podemos establecer reglas para convertir enums a un valor de base de datos correspondiente si usamos la interfaz AttributeConverter. Además, podemos agregar nuevos valores de enum o cambiar los existentes sin romper los datos ya persistidos.
Es sencillo de implementar y supera las desventajas de las opciones presentadas en las secciones anteriores.
7.4. Uso de Enums en JPQL
Ahora veamos lo sencillo que es usar enums en las consultas JPQL.
Para encontrar todas las entidades Articulo con la categoría Categoria.DEPORTE:
String jpql ="select a from Articulo a where a.categoria = com.javhoz.ad.jpa.Categoria.DEPORTE";
List<Articulo> articulos = em.createQuery(jpql, Articulo.class).getResultList();
Es importante destacar que en este caso necesitamos utilizar el nombre completo de la enumeración.
Para consultas dinámicas, podemos utilizar parámetros con nombres:
String jpql ="select a from Articulo a where a.categoria = :categoria";
TypedQuery<Articulo> query = em.createQuery(jpql, Articulo.class);
query.setParameter("categoria", Categoria.TECNOLOGIA);
List<Article> articulos = query.getResultList();
Es la forma más adecuada, pues no se necesita utilizar nombres completamente calificados.
Ejercicio 05.03. Conversores personalizados y enumeraciones
Declara una entidad Persona con atributos:
idPersona.
nombre.
apellidos.
fechaNacimiento de tipo LocalDate.
sexo de tipo enumerado Sexo que puede ser HOMBRE o MUJER.
estadoCivil de tipo enumerado EstadoCivil que puede ser SOLTERO, CASADO, DIVORCIADO o VIUDO.
foto de tipo byte[].
Realiza las conversiones para que:
El nombre y apellidos se guardan en la base de datos como “apellidos, nombre”, con la primera letra de cada palabra en mayúsculas (empleando acceso por campo y por propiedad).
La fecha de nacimiento como un entero que representa la edad de la persona en años (obviamente no es la mejor forma de almacenar la edad, pero quiero que practiquéis con los convertidores), usando anotaciones @PostLoad y @PrePersist. Haz pruebas de comportamiento haciendo consultas, inserciones y actualizaciones.
Las enumeraciones se guardarán como cadenas en el caso de estado civil y como un carácter de ‘H’ o ‘M’ en el caso del sexo. Hazlo con conversores personalizados.
La fotografia se guardará en un campo de tipo BLOB.
Debes completar la entidad Persona y los convertidores necesarios para que funcione correctamente.
Hazlo contra la base de datos H2 y comprueba que los datos se guardan correctamente, creando varios registros y recuperándolos.
8. Tipos temporales: @Temporal
Los tipos temporales se refieren al conjunto de tipos basados en el tiempo que se pueden utilizar en mapeos de estado persistentes.
La lista de tipos temporales admitidos incluye los tres tipos java.sql: java.sql.Date, java.sql.Time y java.sql.Timestamp, así como los dos tipos java.util: java.util.Date y java.util.Calendar, así como los tipos de java.time de Java 8.
Funcionan como cualquier otro tipo de mapeo simple, sin necesidad de consideraciones especiales.
Sin embargo, los dos tipos java.util.Date y java.util.Calendar necesitan metadatos adicionales para indicar cuál de los tipos java.sql de JDBC usar al comunicarse con el controlador JDBC y sólo pueden ser especificados en campos propiedades de estos dos tipo (o subclases). Esto se hace anotándolos con la anotación @Temporal y especificando el tipo JDBC como un valor del tipo enumerado TemporalType.
Tiene un único elemento value que es un valor de la enumeración TemporalType.Hay tres valores enumerados, que representan los tres tipos de la base de datos java.sql:
DATE.
TIME.
TIMESTAMP.
Por ejemplo, con java.util.Date y java.util.Calendar se pueden asignar a columnas de fecha en la base de datos:
En JPA 3.2 y superior, esta anotación está desaprobada (deprecated). Se recomienda utilizar los tipos de la API de fecha y hora de Java 8 (java.time). Si se necesita persistir un tipo de fecha, se debe utilizar la anotación @Convert con un convertidor de atributos.
9. Atributos transitorios
Los atributos que no se pretende que sean persistentes pueden modificarse con el modificador transient en Java o con la anotación @Transient. El tiempo de ejecución del proveedor no aplicará sus reglas de mapeo predeterminadas al atributo en el que se especificó.
Los campos transitorios se utilizan por diversas razones:
Podría ser el caso anteriormente mencionado cuando mezclamos el modo de acceso y no queríamos persistir el mismo estado dos veces.
Otra razón podría ser cuando se desea almacenar en caché algún estado en memoria que no se desea volver a calcular, redescubrir o reinicializar. Por ejemplo siguiente, se usa un campo transient para guardar el nombre específico del idioma para “Employee” de modo que lo imprimamos correctamente donde sea que se muestre. Hemos utilizado el modificador transient en lugar de la anotación @Transient para que si el Employee se serializa de una VM a otra, entonces el nombre traducido se reinicializará para corresponder al idioma de la nueva VM.
En casos en los que el valor no persistente debe conservarse durante la serialización, se debe utilizar la anotación en lugar del modificador.
A continuación se muestra un ejemplo de cómo se utilizaría un campo transitorio:
Cualquier entidad debe tener un mapeo a una clave primaria en la tabla.
@Id indica el identificador de la entidad.
Nota: Cuando el identificador de una entidad está compuesto solo por un atributo, se llama un identificador simple.
10.1. Sobrescritura de la clave primaria
Se puede usar la anotación @Column para sobrescribir el nombre de la columna al que se asigna el atributo ID.
Las claves primarias son insertables, pero no nulas ni actualizables.
Con la anotación @Column, los elementos nullable y updatable no deben ser anulados. Sólo al asignar la misma columna a varios campos/relaciones, se debe establecer el elemento insertable en false.
10.2. Tipos de claves primarias
Los mapeos de @Id generalmente están restringidos a los siguientes tipos:
Tipos primitivos de Java: byte, int, short, long y char.
Clases envolventes de tipos primitivos de Java: Byte, Integer, Short, Long y Character.
Cadena: java.lang.String
Tipo numérico grande: java.math.BigInteger
Tipos temporales: java.util.Date y java.sql.Date, java.util.Calendar y java.sql.Timestamp, además de todos los subtipos y las Java 8 java.time API:
java.time.LocalDate
java.time.LocalTime
java.time.LocalDateTime
java.time.OffsetTime
java.time.OffsetDateTime
Float/Double para claves primarias
Se permiten tipos de punto flotante como float y double, así como las clases envolventes Float y Double y java.math.BigDecimal, pero se desaconsejan debido a la naturaleza del error de redondeo y la poca confiabilidad del operador equals() cuando se aplica a ellos. Utilizar tipos flotantes para claves primarias es arriesgado y definitivamente no se recomienda.
10.3. Generación de claves primarias: @GeneratedValue
El proveedor de persistencia generará un valor de identificador para cada instancia de ese tipo de entidad.
Dependiendo de cómo se genere, es posible que en realidad no esté presente en el objeto hasta que la entidad se haya insertado en la base de datos, hasta después de que se haya producido un flush o la transacción haya finalizado.
Existen 5 tipos estrategias de generación de ID, especificando en el elemento strategy a alguno de los valores de la enumeración GenerationType:
AUTO: el proveedor de persistencia debería seleccionar una estrategia apropiada para la base de datos particular.
IDENTITY: asigna claves primarias para la entidad utilizando una columna de identidad de base de datos.
SEQUENCE: asigna las claves primarias para la entidad utilizando una secuencia de base de datos.
TABLE: asigna claves primarias para la entidad utilizando una tabla de base de datos subyacente para garantizar la unicidad.
UUID: asigna las claves primarias para la entidad mediante la generación de un Identificador Único Universal según la norma RFC 4122. El tipo de atributo debe ser java.util.UUID;
Para obtener más detalles, puedes consultar la documentación oficial en la API de JPA 3.1.
Los generadores de tabla y secuencia pueden definirse específicamente y luego reutilizarse por múltiples clases de entidad.
Estos generadores tienen un nombre y son globalmente accesibles para todas las entidades en la unidad de persistencia.
10.3.1. Generación Automática de ID: GenerationType.AUTO
La estrategia de AUTO el proveedor utilizará cualquier estrategia que desee para generar identificadores.
Se crea un valor de identificador por el proveedor e insertado en el campo id de cada entidad Employee que se persista.
Consejo: no se requiere explícitamente que el campo del identificador de la entidad sea de tipo entero, pero generalmente es el único tipo que genera AUTO. Se recomienda emplear long para abarcar toda la extensión del dominio del identificador generado.
Un inconveniente al usar AUTO es que el proveedor elige su propia estrategia para almacenar los identificadores, pero si elige una estrategia basada en tabla, necesita crear una tabla, por lo que necesita permisos para crear una tabla en la base de datos.
AUTO es realmente una estrategia de generación para desarrollo o prototipado. En cualquier otra situación, sería mejor usar una de las otras estrategias de generación.
10.3.2. Generación de ID utilizando una tabla: GenerationType.TABLE
La forma más flexible y portátil de generar identificadores es utilizar una tabla de base de datos. Se puede adaptar a diferentes bases de datos y permite almacenar múltiples secuencias de identificadores diferentes para diferentes entidades dentro de la misma tabla.
Una tabla de generación de ID debe tener dos columnas:
La primera columna es de tipo cadena y se utiliza para identificar la secuencia del generador en particular. Es la clave primaria para todos los generadores en la tabla.
La segunda columna es de tipo entero y almacena la secuencia de ID real que se está generando. El valor almacenado en esta columna es el último identificador que se asignó en la secuencia.
Cada generador definido representa una fila en la tabla.
Existen varios enfoques para definir un generador de tabla:
El enfoque más sencillo es no definir ningún generador y dejar que el proveedor cree la tabla. Si se utiliza la generación (create) de esquema, se creará; si no, la tabla predeterminada asumida por el proveedor debe ser conocida y debe existir en la base de datos.
Un enfoque más preciso es especificar la tabla que se utilizará para almacenar el ID. Esto se hace definiendo un generador de tabla que no crea tablas en realidad, pues es un generador de identificadores que utiliza una tabla para almacenar los valores del identificador
Aunque en el ejemplo se indica la anotación @TableGenerator anotando el atributo del identificador se puede definir en cualquier atributo o clase.
El elemento name nombra globalmente al generador, lo que nos permite hacer referencia a él en el elemento generator de la anotación @GeneratedValue. Pero no aprovechamos la flexibilidad de la tabla de generación de ID, pues no hemos definido ninguna de las propiedades opcionales.
Independientemente de dónde se defina, estará disponible para toda la unidad de persistencia.
Es una buena práctica definirla localmente en el atributo de ID si solo una clase la está utilizando, y definirla en XML, si se va a utilizar para varias clases.
Elementos de la anotación @TableGenerator:
name: nombre del generador (opcional). El valor por predeterminado es el nombre de la entidad cuando la anotación se produce en una entidad o en una clave primaria.
table: nombre de la tabla que almacena los valores de la secuencia de ID (opcional). El valor por defecto lo elige el proveedor de persistencia.
catalog: catálogo de la tabla (opcional). Catálogo por defecto.
schema: esquema de la tabla (opcional). Esquema por defecto para el usuario actual.
pkColumnName: nombre de la columna de clave primaria en la tabla que identifica de manera única al generador (opcional). Por defecto lo elige el proveedor de persistencia.
valueColumnName: nombre de la columna que almacena el valor real de la secuencia de ID que se está generando (opcional). Por defecto lo elige el proveedor de persistencia.
pkColumnValue: valor de clave primaria en la tabla generadora que distingue este conjunto de valores generados de otros que pueden almacenarse en la tabla. El valor predeterminado es un valor elegido por el proveedor para almacenar en la columna de clave principal de la tabla del generador (opcional).
initialValue: valor inicial de la secuencia de ID (opcional).
allocationSize: tamaño de asignación de la secuencia de ID (opcional).
uniqueConstraints: restricciones de unicidad de la tabla (opcional).
indexes: índices de la tabla (opcional).
Un enfoque más cualificado sería especificar los detalles de la tabla::
Se ha incluido algunos elementos adicionales después del nombre del generador. Después del nombre, hay tres elementos: table, pkColumnName y valueColumnName, que definen la tabla real que almacena los identificadores para Emp_Gen. En el ejemplo:
La tabla se llama ID_GEN, el nombre de la columna de clave primaria (la columna que almacena los nombres de los generadores) se llama GEN_NAME, y la columna que almacena los valores de la secuencia de ID se llama GEN_VAL.
El nombre del generador se convierte en el valor almacenado en la columna pkColumnName para esa fila y es utilizado por el proveedor para buscar el generador y obtener su último valor asignado.
El elemento initialValue que representa el último identificador asignado puede especificarse como parte de la definición del generador, pero la configuración predeterminada de 0 será suficiente en casi todos los casos. Esta configuración solo se utiliza durante la generación de esquemas cuando se crea la tabla. Durante ejecuciones posteriores, el proveedor leerá el contenido de la columna de valores para determinar el próximo identificador a asignar.
Para evitar actualizar la fila cada vez que se solicita un identificador, se utiliza un tamaño de asignación. Esto hará que el proveedor preasigne un bloque de identificadores y luego asignará identificadores desde la memoria según sea necesario hasta que se agote el bloque. Una vez que se agota este bloque, la próxima solicitud de un identificador activará otro bloque de identificadores para preasignar y el valor del identificador se incrementará por el tamaño de asignación. De forma predeterminada, el tamaño de asignación está configurado en 50. Este valor puede anularse para ser más grande o más pequeño mediante el uso del elemento allocationSize al definir el generador.
Ejemplo de cómo definir un segundo generador que se utilizará para entidades de dirección pero que utiliza la misma tabla ID_GEN para almacenar la secuencia de identificadores.
Precisamos indicar el valor que estamos almacenando en la columna de clave en elemento pkColumnValue. Este elemento permite que el nombre del generador sea diferente del valor de la columna:
Especifica un generador de ID de dirección llamado Address_Gen, pero luego define el valor almacenado en la tabla para la generación de ID de dirección como Addr_Gen. El generador también establece el valor inicial en 10000 y el tamaño de asignación en 100.
Si no se ha indicado “create” o “drop-and-create”, la tabla debe existir o crearse en la base de datos a través de algún otro medio y configurarse para estar en este estado cuando la aplicación se inicie por primera vez:
A partir del ejecicio anterior con Persona, haz aque el campo idPersona de tipo Long y genera el identificador con una tabla.
La tabla debe ser compartida con otras entidades que tengan un campo id de tipo Long.
Nombre de la tabla: LONG_ID_GEN
Columnas:
nomePK.
valorPK.
El valor de la columna nomePK para la entidad Persona debe ser PERSONA_ID.
Dale un valor inicial de 1000 y un tamaño de asignación de 100.
Crea otro generador para esa tabla que se utilizará para la entidad Direccion con un valor inicial de 2000 y un tamaño de asignación de 50.
Haz pruebas de inserción de datos.
10.3.3. Generación de ID Utilizando una Secuencia de Base de Datos: GenerationType.SEQUENCE
Muchas bases de datos admiten un mecanismo interno de generación de ID llamado secuencias.
Una secuencia de base de datos se puede utilizar para generar identificadores cuando la base de datos subyacente las admite.
Secuencia de una base de datos
Una secuencia de base de datos es un objeto de base de datos que genera una secuencia de números únicos. Cada vez que se solicita un número de secuencia, se genera el siguiente número de secuencia. Las secuencias de base de datos son muy eficientes y se pueden asignar en bloques. Esto significa que el proveedor puede asignar un bloque de identificadores de la base de datos a la memoria y luego asignar identificadores desde la memoria hasta que se agote el bloque. Una vez que se agota el bloque, el proveedor solicitará otro bloque de identificadores de la base de datos. Esto reduce la cantidad de comunicación necesaria con la base de datos y mejora el rendimiento.
La única diferencia entre usar una secuencia para varios tipos de entidad y usar una para cada entidad sería el orden de los números de secuencia y la posible competencia en la secuencia. La opción más segura es definir un generador de secuencias con nombre y hacer referencia a él en la anotación @GeneratedValue:
Si no se utiliza la generación de esquema y la secuencia se crea manualmente, la cláusula INCREMENT BY debería configurarse para que coincida con el elemento allocationSize o el tamaño de asignación predeterminado de la anotación @SequenceGenerator correspondiente.
Ejercicio 05.05. Generación de ids con una secuencia
Repite el ejercicio anterior con Persona, pero esta vez utiliza una secuencia para generar el identificador en una base de datos H2. Haz pruebas compartiendo la secuencia y sin compartirla.
Si puedes, haz lo mismo con una base de datos PostgreSQL.
10.3.4. Generación de ID utilizando una Identidad de Base de Datos
Muchas bases de datos admiten una columna de identidad de clave primaria, a veces denominada columna autonumérica.
La identidad se usa a menudo cuando las secuencias de bases de datos no son compatibles con la base de datos o porque un esquema heredado ya ha definido que la tabla utilice columnas de identidad.
Generalmente, son menos eficientes para la generación de identificadores objeto-relacional porque no se pueden asignar en bloques y porque el identificador no está disponible hasta después del tiempo de commit.
Para indicar que la generación de IDENTIDAD debe ocurrir, la anotación @GeneratedValue debe especificar una estrategia de generación de IDENTITY:
No hay una anotación de generador para IDENTITY porque debe definirse como parte de la definición del esquema de la base de datos para la columna de clave primaria de la entidad.
La generación de IDENTITY no se puede compartir entre varios tipos de entidades.
El identificador no será accesible hasta después de que se haya realizado la inserción. Es la acción de la inserción la que hace que se genere el identificador. Esto significa que no se puede utilizar el identificador en una relación bidireccional hasta después de que se haya realizado la inserción.
Al usar IDENTITY, algunos proveedores insertan entidades (cuando se invoca el método persist) que están configuradas para usar la generación de IDENTITY, en lugar de esperar hasta el tiempo de commit.__
10.3.5. Generación de ID Utilizando un UUID: GenerationType.UUID
Incorporado en JPA 3.1, el proveedor de persistencia generará un identificador único universal (UUID) para cada instancia de esa entidad.
Ejercicio 05.06. Ampliación de la aplicación de persistencia de una biblioteca
Amplía el ejercicio de la biblioteca para que la entidad Book tenga un identificador generado automáticamente por medio de una tabla.
Además:
Crea una enumeración llamada Categoría con los siguientes valores: NOVELA, POESIA, ENSAYO, TEATRO y OTROS.
Haz que la entidad Book tenga un atributo de tipo Categoría y que se persista en la base de datos como una cadena. Realiza una conversión de la enumeración a cadena y viceversa de modo que guarde la categoría con el nombre en mayúsculas sólo la primera letra y con acentos.
Haz que la columna ISBN sea única, de un tamaño de 13 caracteres y que no pueda ser nula.
Crea un atributo de tipo Calendar para la fecha de publicación del libro y haz que se persista en la base de datos como un tipo DATE.
Crea un atributo transitorio que sea el número de días que han pasado desde la fecha de publicación hasta la fecha actual. Utiliza la clase java.time.LocalDate para obtener la fecha actual.
Crea otro atributo transitorio con el ISBN en versión de 10 dígitos, teniendo en cuenta que el ISBN es un número de 13 dígitos. Para ello, puedes utilizar la clase java.math.BigInteger para realizar la conversión y el siguiente algoritmo:
Elimina los primeros tres dígitos (normalmente 978)
Elimina el último dígito. Ahora tienes nueve dígitos
Ahora necesitas calcular el ‘dígito de control’, que será el décimo dígito de tu ISBN. El objetivo del dígito de control es asegurarse de no haber cometido un error tipográfico: transponer dos dígitos, por ejemplo, o escribir mal uno. Esto es bastante complicado:
Multiplica el primer dígito por 10, el segundo por 9, el tercero por 8 y así sucesivamente, hasta llegar al último dígito (multiplicado por 2).
Ahora tienes una cadena de 9 números nuevos. Agrégalos todos juntos.
Divide esta suma por once. Ahora estás interesado en el resto. Por ejemplo, si la suma fuera 242, que es exactamente 11 x 22, entonces el resto es cero. Si la suma fuera 243, entonces sobraría 1. Tendrás un resto que está entre 0 y 10.
Resta ese resto de 11 para obtener el dígito de control.
Si el resultado es 10, entonces el dígito de control es ‘X’.
Código Java:
publicclassISBN {
publicstaticvoidmain(String[] args) {
String isbn ="978-3-16-148410-0";
String isbn10 = isbn.substring(3, isbn.length() - 1);
System.out.println(isbn10);
BigInteger sum = BigInteger.ZERO;
for (int i = 0; i < isbn10.length(); i++) {
int digit = Character.getNumericValue(isbn10.charAt(i));
sum = sum.add(BigInteger.valueOf(digit).multiply(BigInteger.valueOf(10 - i)));
}
System.out.println(sum);
BigInteger remainder = sum.mod(BigInteger.valueOf(11));
System.out.println(remainder);
BigInteger controlDigit = BigInteger.valueOf(11).subtract(remainder);
System.out.println(controlDigit);
if (controlDigit.intValue() == 10) {
System.out.println("X");
} else {
System.out.println(controlDigit);
}
}
}
Un ejemplo más completo:
publicclassISBNConverter {
publicstaticvoidmain(String[] args) {
String isbn13 ="9780123456789"; // ISBN-13 String isbn10 = convertirISBN13aISBN10(isbn13);
System.out.println("ISBN-10: "+ isbn10);
}
publicstatic String convertirISBN13aISBN10(String isbn13) {
// Verifica si el ISBN-13 proporcionado es válidoif (!esISBN13Valido(isbn13)) {
return"ISBN-13 no válido";
}
// Elimina los primeros 3 dígitos (978 o 979) del ISBN-13 String isbn10Parcial = isbn13.substring(3);
// Calcula el dígito de verificación para el ISBN-10 parcialint suma = 0;
for (int i = 0; i < 9; i++) {
int digito = Character.getNumericValue(isbn10Parcial.charAt(i));
suma += (i + 1) * digito;
}
int digitoVerificador = suma % 11;
char digitoVerificadorChar;
if (digitoVerificador == 10) {
digitoVerificadorChar ='X';
} else {
digitoVerificadorChar = (char) ('0'+ digitoVerificador);
}
// Combina el ISBN-10 parcial con el dígito de verificación calculadoreturn isbn10Parcial + digitoVerificadorChar;
}
publicstaticbooleanesISBN13Valido(String isbn13) {
// Verifica que el ISBN-13 tenga 13 dígitos y comience con "978" o "979"return isbn13.matches("^97[89]\\d{10}$");
}
}
Crea varios libros y pérsistelos en la base de datos (una nueva). Recupéralos y muestra los valores de los datos, incluyendo transitorios.
La mayoría de las entidades necesitan referenciar o tener relaciones con otras entidades. Es lo que produce un modelo gráfico de entidades y relaciones común en las aplicaciones de negocio.
En JPA, las relaciones entre entidades se definen mediante anotaciones en los atributos/propiedades de las entidades.
En este apartado vamos a ver cómo se pueden definir relaciones entre entidades en JPA.
Las anotaciones que se utilizan son las siguientes:
@OneToOne: relación uno a uno.
@OneToMany: relación uno a muchos.
@ManyToOne: relación muchos a uno.
@ManyToMany: relación muchos a muchos.
Además, también se emplean otras anotaciones que permiten concretar y especificar los cuatro tipos de relaciones:
@Embedded: define una relación de tipo embebida (una entidad embebida en otra).
@ElementCollection: definir una relación de tipo colección (una relación uno a muchos en la que hay una dependencia entre las entidades).
@JoinColumn: define el nombre de la columna que se utilizará para la relación.
@JoinTable: permite definir el nombre de la tabla que se utilizará para la relación.
@MapKey: permite definir el nombre de la columna que se utilizará como clave en una relación de tipo mapa.
@OrderBy: nombre de la columna que se utilizará para ordenar los elementos de una relación.
@OrderColumn: nombre de la columna que se utilizará para ordenar los elementos de una relación.
@Index: Permite definir el nombre de la columna que se utilizará para crear un índice en una relación.
@ForeignKey: nombre de la columna que se utilizará para crear una clave foránea en una relación.
@AssociationOverride: nombre de la columna que se utilizará para crear una clave foránea en una relación.
@AttributeOverride: Permite definir el nombre de la columna que se utilizará para crear una clave foránea en una relación.
@EmbeddedId: definir una clave primaria compuesta.
@IdClass: definir una clave primaria compuesta.
1.1. Roles de las entidades en las relaciones
En cada relación hay dos entidades que están relacionadas, y cada entidad se dice que tiene un rol en la relación.
Los dos roles son:
Una entidad tiene un rol de propietario.
otra entidad tiene un rol de inversor.
El rol de propietario determina cómo se actualiza la relación en la base de datos.
El elemento mappedBy en la anotación de la relación designa la propiedad o campo en la entidad que es el propietario de la relación.
1.2. Direccionalidad de las relaciones
El modo más sencillo de implantar relaciones es que una entidad tenga un atributo que referencia a otra entidad, que identifica el papel que juega en la relación.
Además, es usual que la otra entidad tenga un atributo que apunte a la entidad original (relación bidireccional).
Las relaciones entre entidades pueden ser:
Unidireccionales: cuando sólo un atributo apunta a la otra entidad (es el lado propietario).
Bidireccionales: cuando cada entidad tiene un/os atributo/s que referencian a la otra entidad.
Más concretamente:
Una relación bidireccional tiene un lado propietario (owning) y un lado inverso (non-owning).
Una relación unidireccional tiene solo un lado propietario. El lado propietario de una relación determina las actualizaciones de la relación en la base de datos.
Relación unidireccional:
La otra entidad no tiene referencia a la primera entidad. Por ejemplo, en una relación unidireccional uno a muchos, la entidad que representa el lado “uno” de la relación tiene una referencia a la entidad que representa el lado “muchos” de la relación, pero la entidad que representa el lado “muchos” de la relación no tiene referencia a la entidad que representa el lado “uno” de la relación.
Por ejemplo, una relación unidireccional uno a uno entre las entidades Empleado y Dirección se puede definir de la siguiente manera:
@EntitypublicclassEmpleado {
@Idprivateint idEmpleado;
private String nombre;
@OneToOneprivate Direccion direccion; // Empleado tiene una referencia a Direccion// ...}
Se creará una tabla Empleado con una columna direccion_idDireccion que será la clave foránea que referencia a la tabla Direccion. Se dice que Empleado es el propietario de la relación.
La tabla Direccion no tiene referencia a la tabla Empleado.
A veces, las relaciones unidireccionales en el modelo de objetos son un problema en el modelo de la base.
Relación bidireccional:
En una relación bidireccional, cada entidad tiene una referencia a la otra entidad. Por ejemplo, en una relación bidireccional uno a muchos, la entidad que representa el lado “uno” de la relación tiene una referencia a la entidad que representa el lado “muchos” de la relación, y la entidad que representa el lado “muchos” de la relación tiene una referencia a la entidad que representa el lado “uno”
Por ejemplo, Empleado y Proyecto podría ser una relación bidireccional si el empleado tiene referencia de los proyectos en los que trabaja y el Proyecto tiene referencia de los objetos de tipo Empleado que trabajan en el Proyecto.
Ejemplo de relación bidireccional entre las entidades Empleado y Proyecto:
La tabla Empleado no tiene referencia a la tabla Proyecto. Sin embargo, se creará una tabla de unión Empleado_Proyecto que contendrá las claves primarias de ambas tablas.
La ordinalidad indica la necesidad de que exista una entidad destino cuando se crea una entidad.
Sirve para mostrar si la entidad de destino necesita ser especificada cuando se crea la entidad de origen. Dado que la ordinalidad es realmente sólo un valor booleano, también se le conoce como la opcionalidad de la relación.
En términos de cardinalidad, la ordinalidad se indica mediante la cardinalidad siendo un rango en lugar de un valor simple, y el rango comenzaría con 0 o 1 dependiendo de la ordinalidad.
Es más sencillo simplemente indicar que la relación es opcional o obligatoria. Si es opcional, el destino puede no estar presente; si es obligatoria, una entidad de origen sin una referencia a su entidad de destino asociada se encuentra en un estado no válido.
Si existe una asociación entre dos entidades, se debe aplicar una de las siguientes anotaciones de modelado de relaciones a la propiedad persistente correspondiente o al campo de la entidad referenciadora: @OneToOne, @OneToMany, @ManyToOne, @ManyToMany. Para asociaciones que no especifican el tipo de destino (por ejemplo, cuando no se utilizan tipos genéricos de Java para colecciones), es necesario especificar la entidad que es el destino de la relación.
3. Relaciones mono-valuadas: OneToOne y ManyToOne
Son aquellas relaciones en las que la cardinalidad del destino es 1:
Las relaciones mono-valuadas son las que se establecen entre dos entidades y que se pueden representar mediante una única columna en la tabla de la entidad que representa el lado “muchos” de la relación (la entidad tiene un atributo simple que referencia a la otra entidad).
La entidad origen referencia a una entidad destino.
Un ejemplo de una asociación uno a uno sería un Empleado que tiene un Aparcamiento.
Suponiendo que cada empleado tenga asignada su propia plaza de aparcamiento, crearíamos una relación uno a uno desde Empleado hasta Aparcamiento:
Entidad propietaria de la relación:
La entidad Empleado tendría un atributo aparcamiento que referencia a la entidad Aparcamiento y se dice que es la entidad propietaria de la relación (tendrá una clave foránea relacionada con Apartamiento).
Puede verse que la tabla Empleado tiene una columna aparcamiento_idAparcamiento que es la clave foránea que referencia a la tabla Aparcamiento.
Anotación @JoinColumn:
También es posible indicar la columna de la relación por medio de la anotación @JoinColumn, que permite sustituir el nombre de la columna predeterminada, aparcamiento_idAparcamiento, por una nueva:
La entidad objetivo de la relación uno a uno a menudo tiene una relación de vuelta a la entidad fuente; por ejemplo, Aparcamiento tiene una referencia de vuelta al Empleado que lo utiliza. Es lo que se llama a una relación bidireccional uno a uno.
Sólo se necesita añadir un atributo Aparcamiento para que apunte a Empleado:
La tabla de entidad que contiene la columna de unión determina la entidad que es propietaria de la relación.
En una relación bidireccional uno a uno, ambas asignaciones son asignaciones de uno a uno, y cualquiera de los lados puede ser el propietario, por lo que la columna de unión podría terminar en uno u otro lado. Es una decisión de modelado de datos, no una decisión de programación Java.
Ahora tenemos que agregar una referencia de vuelta de Aparcamiento a Empleado. Esto se logra añadiendo la anotación de relación @OneToOne en un atributo empleado. Como parte de la anotación, debemos agregar un elemento mappedBy para indicar que el lado propietario es Empleado, no Aparcamiento.
Dado Aparcamiento es el lado inverso de la relación, no se puede suministrar la información de la columna de unión.
Las dos reglas para asociaciones bidireccionales uno a uno son las siguientes:
La anotación @JoinColumn va en el mapeo de la entidad que está mapeada a la tabla que contiene la columna de unión, o el lado propietario de la relación. Esto podría estar en cualquiera de los lados de la asociación.
El elemento mappedBy debe especificarse en la anotación @OneToOne en la entidad que no define una columna de unión, o el lado inverso de la relación.
Aviso
No es legal tener una asociación bidireccional que tuviera mappedBy en ambos lados, al igual que tampoco incorrecto no tenerlo en ninguno de los lados. La diferencia es que si estuviera ausente en ambos lados de la relación, el proveedor trataría cada lado como una relación unidireccional independiente. Esto estaría bien, excepto que asumiría que cada lado era el propietario y que cada uno tenía una columna de unión.
Si le hubiésemos puesto @JoinColumn a la entidad Aparcamiento, la tabla resultante sería:
A continuación, pondremos un ejercicio de ejemplo de relación uno a uno bidireccional.
Ejercicio 06.01. Relación uno a uno bidireccional Equipo-Entrenador
Vamos a crear una aplicación de equipos de la NBA. Cada equipo tiene un entrenador y cada entrenador tiene un equipo, por lo que la relación es uno a uno bidireccional.
Crea las siguientes entidades:
Equipo: con los atributos idEquipo, nombre, ciudad, conferencia, division, nombreCompleto y abreviatura.
Crea una enumeración Conferencia con los valores ESTE y OESTE.
Crea una enumeración Division con los valores ATLANTICO, CENTRAL, SURESTE, NOROESTE, PACIFICO y SUROESTE.
En la base de datos, la conferencia y la división se guardarán como cadenas:
En una base de datos, las relaciones significan que una tabla referencia a otra. Cuando una columna referencia un clave (primaria) de otra tabla es lo que se denomina “Clave foránea”.
En JPA las claves foráneas se denominan “Join Columns” y, para ello, se emplea la anotación @JoinColum.
@JoinColumn y @JoinTable
La anotación @JoinColumn se utiliza para especificar una columna de clave foránea en una relación, usualmente, el nombre de la relación (name). Si la anotación @JoinColumn no se indica, el nombre de la columna de clave foránea se forma como el nombre de la propiedad o campo de relación de referencia de la entidad o clase embebible “_”; el nombre de la columna de clave primaria referenciada. Por ejemplo, si la relación es departamento en la entidad Empleado, la columna de clave foránea se llamará departamento_idDepartamento.
A veces, las @JoinColumn están dentro de otras tablas llamadas tablas de unión. En estos casos, se utiliza la anotación @JoinTable para especificar el nombre de la tabla de unión. Lo veremos en las relaciones multi-valuadas, como muchos-a-muchos.
Por ejemplo, si quisiéramos que la columna de la relación se llamara idDepartamento en lugar de departamento_idDepartamento, podríamos hacerlo de la siguiente manera:
La columna idDepartamento se añadiría a la tabla Empleado y se referenciaría a la columna idDepartamento de la tabla Departamento.
En la mayoría de las relaciones, independientemente de los lados fuente u origen, uno de los dos lados tiene una columna de clave foránea que referencia la clave primaria de la otra tabla. El lado que tiene la columna de clave foránea es el lado propietario de la relación.
La anotación @JoinColumn dispone de varios elementos:
name: nombre de la columna de clave foránea.
referencedColumnName: nombre de la columna referenciada por la columna de la clave foránea. Por ejemplo: @JoinColumn(name="idDepartamento", referencedColumnName="idDepartamento"). En dónde idDepartamento es el nombre de la columna de clave foránea y idDepartamento es el nombre de la columna referenciada.
nullable: indica si la columna de clave foránea puede ser nula.
unique: indica si la columna de clave foránea debe ser única.
insertable: indica si la columna de clave foránea debe incluirse en las operaciones de inserción.
updatable: indica si la columna de clave foránea debe incluirse en las operaciones de actualización.
columnDefinition: fragmento SQL que se usa para la generación del DDL de la columna de clave foránea.
IMPORTANTE: elemento mappedBy
La ausencia del elemento mappedBy en la anotación @ManyToOne indica que la relación es unidireccional. Si se especifica el elemento mappedBy en la entidad no propietaria (inversa, la que no tiene clave foránea), la relación es bidireccional. Además, su ausencia indica que es el propietario de la relación, mientras que la presencia de mappedBy indica que no es el propietario de la relación.
Ejercicio 06.02. Relación muchos a uno unidireccional Jugador-Equipo
Siguiendo el ejemplo anterior, vamos a crear una relación muchos a uno unidireccional entre Jugador y Equipo.
Para ello debe crear una nueva entidad Jugador con los siguientes atributos:
idJugador: identificador del jugador.
nombre: nombre del jugador.
apellidos: apellidos del jugador.
equipo: equipo al que pertenece el jugador.
altura: altura del jugador (Double).
peso: peso del jugador (Double).
numero: número de camiseta del jugador (SmallInt).
anoDraft: año de elección en el draft (entero).-
numeroDraft: número de elección en el draft (SmallInt).
rondaDraft: ronda de elección en el draft (SmallInt).
posicion: posición en la que juega (base, escolta, alero, ala-pívot, pívot, como enumeración, que debe guardarse como ‘G’, ‘C’, ‘F’, ‘F-C’, ‘C-F’).
pais: país de origen del jugador.
colegio: universidad o equipo en el que jugó.
foto: foto del jugador.
Haz que la relación sea unidireccional, de modo que la entidad Jugador tenga una referencia al Equipo y el nombre de la clave foránea sea idEquipo.
Crea jugadores y añádelos a los equipos que has creado en el ejercicio anterior. Completa la aplicación para que puedas añadir jugadores a los equipos y mostrar los jugadores de un equipo.
Las relaciones multi-valuadas son aquellas en las que la cardinalidad del destino es mayor que uno (muchos). Esto es, cuando una entidad puede estar asociada con más de una instancia de la otra entidad:
En este caso, se utilizan colecciones para representar las relaciones y es importante anotar la parte de la colección con: @OneToMany o @ManyToMany.
IMPORTANTE: mappedBy en relaciones OneToMany (y ManyToMany)
En las relaciones @OneToMany, la entidad que representa el lado “uno” de la relación suele estar indicada con el elemento mappedBy en la anotación @OneToMany para indicar que el lado inverso de la relación es el propietario de la relación y el que tiene la columna de clave foránea.
Aunque en una relación @ManyToMany no es preciso indicar quién es la entidad propietaria con mappedBy, es recomendable hacerlo para evitar problemas, y, además, poder especificar el nombre de la tabla de unión (en la que se almacenan las claves foráneas de ambas entidades).
4.1. @OneToMany
4.1.1. @OneToMany bidireccional
Cuando una entidad está asociada a una colección, Collection, (java.util.Collection) de otras entidades, se utiliza la anotación @OneToMany.
Una relación bidireccional one-to-many se establece mediante la anotación @OneToMany e implica una relación @ManyToOne en el lado opuesto de la relación, pues siempre implica una relación many-to-one en el lado opuesto de la relación.
En este tipo de relaciones, son (casi) siempre bidireccionales y el lado “UNO”, normalmente, NO es el propietario de la relación.
Por ejemplo, entre Departamento y Empleado:
En el siguiente ejemplo, la entidad Departamento tiene una colección de Empleado y delega la responsabilidad de la relación a la entidad Empleado por el elemento mappedBy (la tabla Departamento no tendrá referencia a la tabla Empleado y la tabla Empleado tendrá una referencia a la tabla Departamento):
@EntitypublicclassDepartamento {
@Idprivateint idDepartamento;
private String nombre;
@OneToMany(mappedBy="departamento") // Empleado debe tener un atributo "departamento"private Collection<Empleado> empleados;
// ...}
@EntitypublicclassEmpleado {
@Idprivateint idEmpleado;
private String nombre;
privatelong salario;
@ManyToOne@JoinColumn(name="idDepartamento") // Nombre de la columna de clave foráneaprivate Departamento departamento;
// ...}
El resultado es una tabla Empleado con la clave foránea del Departamento que referencia a la tabla Departamento:
La única diferencia con la relación @ManyToOne es que se añade el atributo mappedBy en la anotación @OneToMany, que indica que el lado inverso de la relación es el propietario de la relación.
Es importante saber lo siguiente en las relaciones one-to-many o many-to-one bidireccionales:
El lado Many-To_One, que tiene la columna de clave foránea (@JoinColumn) es el lado propietario de la relación.
El lado One-To-Many, que tiene la anotación @OneToMany debe tener el elemento mappedBy, pues es el lado inverso de la relación. Si no se especifica mappedBy, el proveedor puede tratarlo como una relación unidireccional uno a muchos, que se define con una tabla intermedia.
Omisión de mappedBy en relaciones OneToMany
IMPORTANTE: si no se indica el elemento mappedBy en la anotación @OneToMany el proveedor puede tratarlo como una relación unidireccional uno a muchos, que se define con una tabla intermedia.
Es un error común no especificar mappedBy en el lado inverso de una relación bidireccionaluno a muchos . Si no se especifica mappedBy, el proveedor puede crear una tabla intermedia para la relación, que no es lo que se desea:
Sólo en el caso de relaciones one-to-many unidireccionales se puede omitir mappedBy. Pues en ese caso la parte muchos no tiene referencia a la parte uno.
4.1.2 @OneToMany unidireccional
En algunos casos, la relación uno a muchos no tiene elemento mappedBy en la anotación @OneToMany, lo que indica que la relación es unidireccional. En ese caso la entidad muchos no tiene referencia a la entidad uno y sólo existe una colección en la entidad uno que referencia a la entidad muchos.
Por ejemplo, entre Empleado y Telefono podría verse como una relación unidireccional:
JPA permite el uso de genéricos, por lo que se puede (y se debe) especificar el tipo de colección que se utilizará para la relación. Por ejemplo, Collection, List, Set, Map, etc. sin parametrizar.
En el caso de que se quiera emplear genéricos sin parametrizar, se debe especificar el tipo de la relación con la anotación @OneToMany y el elemento targetEntity:
Cuando ambos lados de una relación de entidades tienen una asociación de una colección, se trata de una relación Muchos-a-muchos y se utiliza la anotación @ManyToMany.
Por ejemplo, entre Empleado y Proyecto:
Ambos lados se mapean con la anotación @ManyToMany, especificando los parámetros de la tabla de unión con la anotación @JoinTable (evitamos los valores por defecto):
@EntitypublicclassEmpleado {
@Idprivateint idEmpleado;
private String nombre;
privatelong salario;
@ManyToMany@JoinTable(name ="EmpleadoProyecto",
joinColumns =@JoinColumn(name="idEmpleado"), // Si hubiera más de una columna, se especificaría con un array: // joinColumns = {@JoinColumn(name="idEmpleado"), @JoinColumn(name="idOtraColumna")} inverseJoinColumns =@JoinColumn(name="idProyecto"))
private Collection<Proyecto> proyectos;
// ...}
joinColumns es un array (JoinColumn[] joinColumns) y podría tener más de un elemento, si la tabla de unión tiene más de una columna de clave foránea:
Queda pendiente que hagáis pruebas sin especificar @JoinTable para ver cómo se comporta con los valores por defecto. Del mismo modo, que sucede si no se especifica mappedBy en el lado inverso de la relación.
Si no se indica la tabla de unión, el proveedor de JPA creará una tabla de unión con los valores por defecto:
@JoinTable permite declarar toda la información sobre las columnas de la tabla de unión, como el nombre de la tabla, el nombre de las columnas de clave foránea, etc.
Los nombres de las columnas son en plural porque podría haber varias columnas por cada clave foránea (en el caso de una clave primaria con varias columnas).
IMPORTANTE: mappedBy en relaciones ManyToMany
Hay una importante diferencia entre las relaciones many-to-many y one-to-many:
Cuando muchos-a-muchos es bidireccional, ambos lados de la relación son muchos-a-muchos.
NO HAY columnas @JoinColumn en ninguna de las entidades, pues no hay un lado propietario de la relación y la única forma de mapear una relación muchos-a-muchos es con una tabla de unión (@JoinTable).
En una relación many-to-many, no hay un lado propietario de la relación. Ambos lados de la relación son iguales. Por ello, hay que especificar el lado propietario de la relación con el elemento mappedBy en la anotación @ManyToMany en el lado inverso de la relación.
Da igual cuál es el lado propietario, pero sólo se puede especificar mappedBy en uno de los lados de la relación.
5. Nombre de la columna de Clave foránea
El nombre de la columna de clave foránea (o de las tablas) se especifica con el elemento name de la anotación @JoinColumn. Si no se especifica, el nombre de la columna de clave foránea se genera automáticamente.
El nombre de la tabla en la que se encuentra depende del contexto.
Dónde se encuentra la columna de clave foráneadepende del tipo de relación y de la estrategia de mapeo de clave foránea:
Si la unión es para un mapeo OneToOne o ManyToOne utilizando una estrategia de mapeo de clave externa, la columna de clave externa está en la tabla de la entidad fuente o embebible.
Si la unión es para un mapeo unidireccional OneToMany utilizando una estrategia de mapeo de clave externa, la clave externa está en la tabla de la entidad objetivo.
Si la unión es para un mapeo ManyToMany o para un mapeo OneToOne o ManyToOne/OneToMany bidireccional utilizando una tabla de unión, la clave externa está en una tabla de unión.
Si la unión es para una colección de elementos, la clave foránea está en una tabla de colección. La colección de elementos lo veremos en otro apartado.
Ejercicio 06.03. Relación ManyToMany unidireccional Jugador-Posición
Vamos a crear una relación muchos a muchos unidireccional entre Jugador y Posicion. Para eso debes crear una nueva entidad Posicion con los siguientes atributos:
idPosicion: identificador de la posición (Long).
nombre: nombre de la posición (String, tamaño máximo 50).
abreviatura: abreviatura de la posición (String, tamaño máximo 3).
descripcion: descripción de la posición (String, tamaño máximo 255).
Haz que la relación sea unidireccional, de modo que la entidad Jugador tenga una colección de Posicion y el nombre de la tabla de unión sea JugadorPosicion.
Crea posiciones y añádelas a los jugadores que has creado en el ejercicio anterior.
Ejercicio 6.4. Mapeo de una base de datos de juegos
Migración de base de datos H2 entre versiones
En la base de datos origen se ejecuta el siguiente script:
SCRIPT TO'<ruta-al-archivo-backup>/backup.sql';
En la base de datos destino se ejecuta el siguiente script:
Cuyos datos se ajustan al formato del siguiente JSON (ejemplo). Debes tener en cuenta que no se ha creado la tabla de requeriminetos mínimos, pero se puede hacer si se desea en una nueva tabla de la base de datos, relacionada, uno a uno:
{
"id": 452,
"title": "Call Of Duty: Warzone",
"thumbnail": "https:\/\/www.freetogame.com\/g\/452\/thumbnail.jpg",
"status": "Live",
"short_description": "A standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare.",
"description": "Call of Duty: Warzone is both a standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare. Warzone features two modes \u2014 the general 150-player battle royle, and \u201cPlunder\u201d. The latter mode is described as a \u201crace to deposit the most Cash\u201d. In both modes players can both earn and loot cash to be used when purchasing in-match equipment, field upgrades, and more. Both cash and XP are earned in a variety of ways, including completing contracts.\r\n\r\nAn interesting feature of the game is one that allows players who have been killed in a match to rejoin it by winning a 1v1 match against other felled players in the Gulag.\r\n\r\nOf course, being a battle royale, the game does offer a battle pass. The pass offers players new weapons, playable characters, Call of Duty points, blueprints, and more. Players can also earn plenty of new items by completing objectives offered with the pass.",
"game_url": "https:\/\/www.freetogame.com\/open\/call-of-duty-warzone",
"genre": "Shooter",
"platform": "Windows",
"publisher": "Activision",
"developer": "Infinity Ward",
"release_date": "2020-03-10",
"freetogame_profile_url": "https:\/\/www.freetogame.com\/call-of-duty-warzone",
"minimum_system_requirements": {
"os": "Windows 7 64-Bit (SP1) or Windows 10 64-Bit",
"processor": "Intel Core i3-4340 or AMD FX-6300",
"memory": "8GB RAM",
"graphics": "NVIDIA GeForce GTX 670 \/ GeForce GTX 1650 or Radeon HD 7950",
"storage": "175GB HD space" },
"screenshots": [
{
"id": 1124,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-1.jpg" },
{
"id": 1125,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-2.jpg" },
{
"id": 1126,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-3.jpg" },
{
"id": 1127,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-4.jpg" }
]
}
a) Crea entidades JPA en Java para las tablas de la base de datos, con las siguientes características:
Genero: con los atributos idGenero, nombre. La clave es autonumérica.
Plataforma: con los atributos idPlataforma y nombre. La clave es autonumérica. Nota: si se hubiese declarado como enumeración, para poder mapear una enumeración en una tabla independiente, obligaría a crear una entidad independiente con el idPlataforma y el nombre. Sin embargo, en este caso, se podría mapear la enumeración directamente en la tabla Juego o declararla como una clase y no como una enumeración.
Juego: con todos los atributos de la tabla Juego, incluyendo la relación con Genero y Plataforma. La clave primaria, idJuego, no es autogenerada, es asignada. Ten en cuenta que la relación con la tabla Imagen se trata de una relación uno a muchos, por lo que se deberá declarar una colección de imágenes. Además, el idGenero y el idPlataforma son claves foráneas de las entidades y no deben declararse como atributos de la entidad Juego, sino como objetos del tipo de las entidades Genero y Plataforma.
Imagen: con los atributos idImagen (no autogenerada), Juego (relacionada con la entidad Juego @OneToOne), url, imagen (tipo byte[]).
RequisitosSistema: relacionada con la tabla Juego. Atributos: idJuego (PK), sistemaOperativo (su nombre no coincide con la columna de la tabla), almacenamiento, graficos, memoria, procesador y su relación juego. Debe emplearse una clave comparta con el idJuego. Para ello debe emplearse la anotación: @MapsId: @MapsId("idJuego").
b) Haz una sencilla aplicación que cree un juego y lo persista en la base de datos. Ten en cuenta que las claves no son autonuméricas
Ejemplo de juegos: https://www.freetogame.com/api/game?id=X, pasándole el id del Juego, desde 1 al número de juegos que consideres. Ten en cuenta que el juego podría no existir devolviendo:
{"status":0,"status_message":"No game found with that id"}
Bases de datos
6. Claves compartidas en relaciones uno a uno
Una clave compartida es una clave primaria que se comparte entre dos o más entidades. Una clave compartida se puede mapear con la anotación @MapsId y se emplea para relaciones uno a uno.
En este caso el identificador de un solo atributo es la clave foránea de la relación.
Por ejemplo, en una relación bidireccional uno a uno entre las entidades Empleado y HistorialEmpleado. Dado que solo hay un HistorialEmpleado por Empleado, podríamos decidir compartir la clave primaria (la clave primaria de HistorialEmplado sería la misma que Empleado).
Si HistorialEmpleado es la entidad dependiente, indicamos que la clave foránea de la relación es el identificador anotando la relación con @Id y @OneToOne. (En realidad suele escribirse la anotación @MapsId se coloca en el atributo de relación para indicar que también está mapeando el atributo de ID).
El tipo de clave primaria de HistorialEmpleado va a ser del mismo tipo que Empleado, por lo que si Empleado tiene un identificador simple de tipo entero, entonces el identificador de HistorialEmpleado también será un entero.
Si Empleado tiene una clave primaria compuesta, ya sea con una clase ID o una clase ID incrustada, entonces HistorialEmpleado compartirá la misma clase ID (y también debería estar anotada con la anotación @IdClass).
El problema es que esto choca con la regla de la clase ID que dice que debe haber un atributo coincidente en la entidad por cada atributo en su clase ID. Esta es la excepción a la regla, debido al hecho mismo de que la clase ID se comparte entre ambas entidades, principal y dependiente.
Generalmente, también se podría desear que la entidad contenga un atributo de clave primaria además del atributo de relación, con ambos atributos mapeados a la misma columna de clave foránea en la tabla.
Aunque el atributo de clave primaria es innecesario en la entidad podría querer definirse por separado para un acceso más fácil. A pesar de que los dos atributos se mapean a la misma columna de clave foránea (que también es la columna de clave primaria), el mapeo no tiene que duplicarse en ambos lugares. La anotación @Id se coloca en el atributo de identificación, y @MapsId anota el atributo de relación para indicar que también está mapeando el atributo de ID:
@EntitypublicclassHistorialEmpleado {
// ...@Idint idEmpleado;
@MapsId// Indica que el atributo de relación también mapea el atributo de ID@OneToOne@JoinColumn(name="idEmpleado")
private Empleado empleado;
// ...}
Hay un par de puntos adicionales que vale la pena mencionar sobre @MapsId:
La relación anotada con @MapsId define el mapeo para el atributo de identificación también. Si no hay una anotación @JoinColumn que anule en el atributo de relación, entonces la columna de unión se asignará por defecto (nombreEnidad_idEntidad). En el ejemplo anterior, si se eliminara la anotación @JoinColumn, tanto el atributo empleado como el idEmpleado se mapearían a la columna de clave foránea predeterminada Empleado_idEmpleado (suponiendo que la columna de clave primaria en la tabla Empleado fuera idEmpleado).
Aunque el atributo de identificación comparte el mapeo de la base de datos definido en el atributo de relación, desde la perspectiva del atributo de identificación, es realmente un mapeo de solo lectura. Las actualizaciones o inserciones en la columna de clave foránea de la base de datos solo ocurrirán a través del atributo de relación. Esta es una de las razones por las que siempre se debe establecer las relaciones padre antes de intentar persistir una entidad dependiente (Debes persistir, por ejemplo, primero Empleado y luego HistorialEmpleado).
IMPORTANTE: Claves compartidas
Nota: No intentes establecer solo el atributo de identificación (y no el atributo de relación) como un medio para atajar la persistencia de una entidad dependiente. Algunos proveedores pueden tener soporte especial para hacer esto, pero no garantizará de manera portátil que la clave foránea se escriba en la base de datos. El atributo de identificación se completará automáticamente por el proveedor cuando se lea una instancia de entidad de la base de datos o cuando se realiza un flush/commit. Sin embargo, no se puede asumir que esté presente al llamar primero a persist() en una instancia a menos que el usuario lo establezca explícitamente.
6.2. PrimaryKeyJoinColumn y PrimaryKeyJoinColumns
La anotación @PrimaryKeyJoinColumn se utiliza para especificar una columna de clave primaria de una tabla de unión. Esto es, cuando la clave foránea es la clave primaria de la tabla de unión. Se usa en relaciones uno a uno y solo se puede usar en la entidad propietaria de la relación.
@PrimaryKeyJoinColumns se utiliza para especificar varias columnas de clave primaria de una tabla de unión.
Por ejemplo:
@EntitypublicclassEmpleado {
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
privateint idEmpleado;
private String nombre;
privatelong salario;
@OneToOne@PrimaryKeyJoinColumn// Indica que la columna de clave primaria de la tabla de unión es la misma que la clave primaria de la tabla de la entidadprivate HistorialEmpleado historial;
// ... }
En este caso, la clave primaria de la tabla HistorialEmpleado sería la clave foránea de la tabla Empleado.
En el segundo código, la entidad HistorialEmpleado tiene un campo @Id que actúa como clave primaria de la tabla, por lo que no tiene sentido usar @PrimaryKeyJoinColumn en este caso. La entidad HistorialEmpleado tiene un campo @Id que actúa como clave primaria de la tabla.
Ejercicio 6.5. Claves compartidas en relaciones uno a uno
Comprueba el funcionamiento de la anotación @PrimaryKeyJoinColumn en una relación uno a uno entre Persona y Departamento. Crea las entidades y realiza pruebas de persistencia.
Persona: idPersona (IDENTITY), nombre, departamento (uno a uno con anotación de @PrimaryKeyJoinColumn)
Departamento: idDepartamento (IDENTITY), nombre.
Modifica el ejercicio para que sea bidireccional con @OneToOne y @MapsId en la entidad Departamento y como propietaria de la relación.
Un objeto embebido es un objeto que no tiene identidad propia y que es parte de una entidad.
Los objetos embebidos se utilizan para modelar datos compuestos.
Es parte del estado de una entidad que ha sido extraído y modelado como un objeto independiente.
En Java los objetos embebidos se modelan como clases normales y se anotan con @Embeddable. Sin embargo, en la base de datos, los objetos embebidos se almacenan en la misma tabla que la entidad que los contiene, como cualquier otro de sus atributos.
@Embeddable y @Embedded
La anotación @Embeddable se utiliza para indicar que una clase es un objeto embebido. La anotación @Embedded se utiliza para indicar que un atributo de una entidad es un objeto embebido.
Aunque los objeto embebidos son referenciados por entidades, no se consideran relaciones entre entidades.
Aunque pudiera parecer contradictorio separar un objeto de una entidad y luego volver a unirlo, hay varias razones por las que esto es útil:
Los objetos embebidos pueden ser reutilizados. Si tienes un objeto embebido que representa una dirección, por ejemplo, puedes reutilizarlo en cualquier entidad que necesite una dirección.
Aunque los tipos embebidos pueden ser compartidos o reutilizados, las instancias no. Una instancia de objeto embebido pertenece a la entidad que la referencia; y ninguna otra instancia de entidad, de ese tipo de entidad o de cualquier otro, puede hacer referencia a la misma instancia embebida.
Un ejemplo de reutilización es la información de la dirección postal:
classDiagram
class Direccion {
-String calle
-String ciudad
-String provincia
-String codigoPostal
}
class Empleado {
-long idEmpleado
-String nombre
-long salario
-Direccion direccion
}
class Compañia {
-String nombre
-Direccion direccion
}
Empleado o-- Direccion
Compañia o-- Direccion
La tabla Empleado:
erDiagram
Empleado {
idEmpleado int
nombre varchar
salario int
calle varchar
ciudad varchar
provincia varchar
codigoPostal varchar
}
idEmpleado
nombre
salario
calle
ciudad
provincia
codigoPostal
1
Pepe
1500
C/1
Santiago
A Coruña
15706
2
Xan
2200
C/2
Vigo
Pontevedra
36201
Una tabla Empleado que contiene una mezcla de información básica del empleado, así como columnas que corresponden a la dirección postal del empleado. Las columnas calle, ciudad, provincia y codigoPostal se combinan lógicamente para formar la dirección (clase Direccion).
En el modelo de objetos, es una excelente candidata para ser “abstracta” en un tipo embebido Direccion en lugar de incorporar cada atributo en la clase de la entidad. La clase de entidad solo tendría un atributo de dirección que apunta a un objeto embebido de tipo Address. La figura muestra cómo Empleado y Direccion se relacionan entre sí.
Al persistir una instancia de Empleado, se accede a los atributos del objeto Direccion como si estuvieran presentes en Empleado.
Las asignaciones de columnas en el tipo Direccion realmente se refieren a las columnas de la tabla Empleado, aunque estén en una clase separada.
Objetos embebidos o entidades
Es una decisión de diseño el uso de objetos embebidos. Si se precisa crear relaciones con ellos o desde ellos, no los uses. Los objetos embebidos no están destinados a ser entidades y tan pronto como comiences a tratarlos como entidades, probablemente deberías convertirlos en entidades de primera clase si el modelo de datos lo permite.
No es portátil definir objetos embebidos como parte de jerarquías de herencia. Una vez que comienzan a heredarse entre sí, la complejidad de su incorporación aumenta y la relación costo-beneficio disminuye.
Una clase Direccion podría ser reutilizada tanto en las entidades Empleado como Companía (como hemos indicado en la imagen anterior).
Aunque tanto las clases Empleado como Compañía contiene la clase Direccion, cada instancia de Direccion será utilizada solo por una única instancia de Empleado o Compañía.
2. Sustitución de atributos embebidos: @AttributeOverride
Como las asignaciones de columnas del tipo embebido Direccion se aplican a las columnas de la entidad contenedora (en tablas diferentes), las tablas de entidades podrían tener nombres de columna diferentes para los mismos campos.
erDiagram
Empleado {
idEmpleado int
nombre varchar
salario int
calle varchar
ciudad varchar
provincia varchar
codigoPostal varchar
}
Compania {
nombre varchar
calle varchar
ciudad varchar
prov varchar
codPostal varchar
}
La tabla Empleado coincide con los atributos predeterminados y mapeados del tipo Direccion, pero la tabla Compania se ha modificado con otros nombres de provincia y código postal:
Como se muestra en el ejemplo, para los cambios de nombres en los atributos se puede emplear la anotación @AttributeOverride.
En la declaración de la entidad se emplea la anotación @AttributeOverride para cada atributo del objeto embebido que queremos renombrar en la entidad. Elementos requeridos:
name: el nombre del campo o propiedad embebido en la entidad.
column: la columna a la que se está asignando el atributo en la tabla de la entidad. Se especifica en forma de una anotación @Column anidada.
2.1. Sustitución de múltiples atributos embebidos: @AttributeOverrides
Dado que la anotación @AttributeOverride puede repetirse, no es obligatorio el uso de la anotación @AttributeOverrides. El siguiente ejemplo muestra el uso de Direccion tanto en Empleado como en Compañía. La entidad Empleado utiliza el tipo Direccion sin cambios, pero la entidad Compañía sobrescribe para asignar los atributos provincia y codigoPosta de Direccion a las columnas prov y codPostal de la tabla Companía.
Crea una aplicación con JPA para la gestión de películas y series.
Crea una clase InfoContenido con los siguientes atributos:
titulo (String): de tamaño 100.
genero (String): de tamaño 50.
pais (String): de tamaño 2.
duracion (int): duración en minutos.
año (int): año.
sinopsis (String): de tamaño clob.
Crea una entidad Serie con los siguientes atributos:
idSerie (long): identificador de la serie. Secuencia.
informacion (de tipo InfoContenido)
fechaEstreno (LocalDate).
temporadas (int): número de temporadas.
capitulos (int)
directores (lista de String).
Crea una entidad Pelicula con los siguientes atributos:
idPelicula (long): identificador de la película. Secuencia.
informacion (de tipo InfoContenido)
La entidad Serie y Pelicula deben tener el atributo informacion como un objeto embebido.
La entidad Pelicula el atributo pais debe ser renombrado a paisPelicula.
El atributo directores debe guardarse en una nueva tabla, como una colección con la anotación @ElementCollection (busca información sobre esta anotación).
La fecha de estreno, fechaEstreno, de la serie debe guardarse en formato numérico (YYYYMMDD).
4. ManyToMany usando una clave compuesta
A modo de ejemplo, tenemos una relación de muchos a muchos entre dos entidades, Estudiante y Curso. Un estudiante puede inscribirse en varios cursos, y un curso puede tener varios estudiantes inscritos.
Además, queremos que los estudiantes califiquen los cursos, que sería un atributo de la relación. Un estudiante puede calificar cualquier número de cursos, y cualquier número de estudiantes puede calificar el mismo curso.
Una clave primaria compuesta, también llamada clave compuesta, es una combinación de dos o más columnas para formar una clave primaria para una tabla.
En JPA, tenemos dos opciones para definir las claves compuestas: las anotaciones @IdClass y @EmbeddedId.
Para definir las claves primarias compuestas, debemos seguir algunas reglas:
La clase de clave primaria compuesta debe ser pública.
Debe tener un constructor sin argumentos.
Debe definir los métodos equals() y hashCode().
Debe ser Serializable.
4.1. Modelando Atributos de Relación
Queremos permitir que los estudiantes califiquen los cursos. Un estudiante puede calificar cualquier número de cursos, y cualquier número de estudiantes puede calificar el mismo curso. Por lo tanto, también es una relación de muchos a muchos.
A diferencia de las otras relaciones muchos a muchos, necesitamos almacenar la puntuación de calificación que el estudiante dio al curso, en la tabla intermedia.
¿Dónde podemos almacenar esta información? No podemos ponerla en la entidad Estudiante ya que un estudiante puede dar diferentes calificaciones a diferentes cursos. De manera similar, almacenarlo en la entidad Curso tampoco sería una buena solución.
Esta es una situación en la que la relación en sí tiene un atributo.
Usando este ejemplo, adjuntar un atributo a una relación se ve así en un diagrama ER:
Podemos modelarlo casi de la misma manera que la relación de muchos a muchos sencilla. La única diferencia es que añadimos un nuevo atributo a la tabla de unión:
4.2. Creando una Clave Compuesta en JPA: @Embeddable
La implementación de una relación de muchos a muchos sencilla fue bastante directa, pero no podemos agregar una propiedad a una relación de esa manera porque conectamos las entidades directamente. Por lo tanto, no teníamos forma de añadir una propiedad a la relación en sí.
Dado que mapeamos los atributos de la base de datos a campos de clase en JPA, necesitamos crear una nueva clase de entidad para la relación:
Cada entidad JPA necesita una clave primaria. Dado que la clave primaria es una clave compuesta, tenemos que crear una nueva clase para la clave, ClaveCalificacionCurso, que contendrá las diferentes partes de la clave:
@EmbeddableclassClaveCalificacionCursoimplements Serializable {
@Column(name ="idEstudiante")
private Long idEstudiante;
@Column(name ="idCurso")
private Long idCurso;
// constructores estándar, getters y setters// implementación de hashcode y equals}
Ten en cuenta que una clase de clave compuesta debe cumplir con algunos requisitos clave:
Debe marcarse con @Embeddable.
Debe implementar java.io.Serializable.
Necesitamos proporcionar una implementación de los métodos hashcode() e equals().
4.3. Utilizando una Clave Compuesta en JPA. @EmbeddedId
Usando esta clase de clave compuesta, podemos crear la clase de entidad que modela la tabla de unión:
Este código es muy similar a una implementación usual de entidad. Sin embargo, tenemos algunas diferencias clave:
Usamos @EmbeddedId para marcar la clave primaria, que es una instancia de la clase ClaveCalificacionCurso (recuerda que antes añadimos la anotación @Embeddable a esta clase).
Marcamos los campos estudiante y curso con @MapsId.
@MapsId significa que vinculamos esos campos a una parte de la clave y son las claves externas de una relación de muchos a uno.
Después de esto, podemos configurar las referencias inversas en las entidades Estudiante y Curso como antes:
Ten en cuenta que hay una forma alternativa de usar claves compuestas: la anotación @IdClass.
4.4. Características Adicionales
Configuramos las relaciones con las clases Estudiante y Curso como @ManyToOne, porque con la nueva entidad descompusimos estructuralmente la relación de muchos a muchos en dos relaciones de muchos a uno.
Ahora tenemos dos relaciones de muchos a uno. En otras palabras, no hay ninguna relación de muchos a muchos en un RDBMS. Llamamos a las estructuras que creamos con tablas de unión relaciones de muchos a muchos porque eso es lo que modelamos.
Además, es más claro si hablamos de relaciones de muchos a muchos porque esa es nuestra intención. Mientras tanto, una tabla de unión es solo un detalle de implementación; realmente no nos importa.
Esta solución tiene una característica adicional que aún no hemos mencionado. La solución simple de muchos a muchos crea una relación entre dos entidades. Por lo tanto, no podemos expandir la relación a más entidades. Pero no tenemos este límite en esta solución: podemos modelar relaciones entre cualquier número de tipos de entidades.
Cuando varios profesores pueden enseñar un curso, los estudiantes pueden calificar cómo un profesor específico enseña un curso específico. De esa manera, una calificación sería una relación entre tres entidades: un estudiante, un curso y un profesor.
Ejercicio 7.2. Clave compuesta en una relación de muchos a muchos.
Data la aplicación de gestión de películas y series, añade dos nuevas entidades: Usuario y Calificacion que permita a los usuarios calificar las películas.
Crea una clase Usuario con los siguientes atributos:
idUsuario (long): identificador del usuario. Secuencia.
nombre (String): nombre del usuario.
email (String): email del usuario.
password (String): contraseña del usuario.
fechaRegistro (LocalDate): fecha de registro.
Crea una clase Calificacion con los siguientes atributos:
calificacion (int): calificación del contenido, con valores de 10 a 0.
fechaCalificacion (LocalDate): fecha de la calificación.
comentario (String): comentario de la calificación.
Además, debe estar relacionado con las entidades Usuario, Pelicula y Serie. Como un usuario puede calificar varias películas y series, y una película o serie puede ser calificada por varios usuarios, es una relación de muchos a muchos. No es preciso que califique series, pues el caso de uso es similar al de las películas.
La clave primaria de la tabla Calificacion debe ser compuesta por los atributos idUsuario, idPelicula.
4.5. La anotación @IdClass
Digamos que tenemos una tabla llamada Cuenta y tiene dos columnas, numeroCuenta y tipoCuenta, que forman la clave compuesta. Ahora tenemos que mapearlo en JPA.
Según la especificación de JPA, creemos una clase IdCuenta con estos campos de clave primaria:
A continuación, asociemos la clase IdCuenta con la entidad Cuenta.
Para hacer eso, necesitamos anotar la entidad con la anotación @IdClass. También debemos declarar los campos de la clase IdCuenta en la entidad Cuenta y anotarlos con @Id:
@Entity@IdClass(IdCuenta.class)
publicclassCuenta {
@Idprivate String numeroCuenta;
@Idprivate String tipoCuenta;
// otros campos, getters y setters}
Mapeando el estado de la relación
En ocasiones, una relación tiene un estado asociado. Por ejemplo, supongamos que queremos mantener la fecha en la que un empleado fue asignado a trabajar en un proyecto (atributo de la relación). Almacenar el estado en el empleado es posible, pero menos útil, ya que la fecha está realmente asociada a la relación del empleado con un proyecto particular (una sola entrada en la asociación de muchos a muchos). Sacar a un empleado de un proyecto debería hacer que la fecha de asignación desaparezca, por lo que almacenarla como parte del empleado significa que tenemos que asegurarnos de que ambos sean consistentes entre sí, lo cual puede ser molesto. En UML, mostraríamos este tipo de relación usando una clase de asociación. El siguiente esquema muestra un ejemplo de esta técnica, en la que Employee tiene un id (Long), un nombre (string) y una salario (long), un proyecto tiene un id (Long) y un nombre (string), y la relación entre ellos tiene una fecha de inicio (Date).
classDiagram
class Empleado {
-Long id
-String nombre
-long salario
}
class Proyecto {
-Long id
-String nombre
}
class AsignacionProyecto {
-Date fechaInicio
}
Empleado o-- AsignacionProyecto
Proyecto o-- AsignacionProyecto
In the database, everything is rosy because we can simply add a column to the join
table. The data model provides natural support for relationship state. Figure 10-7 shows
the many-to-many relationship between EMPLOYEE and PROJECT with an expanded
join table.
En la base de datos, todo es perfecto porque podemos simplemente añadir una columna a la tabla de unión. El modelo de datos proporciona un soporte natural para el estado de la relación. La figura anterior muestra la relación muchos a muchos entre EMPLEADO y PROYECTO con una tabla de unión expandida.
erDiagram
EMPLEADO {
id int
nombre varchar
salario int
}
PROYECTO {
id int
nombre varchar
}
ASIGNACION_PROYECTO {
EMP_ID int
PROJECT_ID int
START_DATE date
}
Cuando llegamos al modelo de objetos, sin embargo, se vuelve mucho más problemático. El problema es que Java no tiene un soporte inherente para el estado de la relación. Las relaciones son solo referencias a objetos o punteros; por lo tanto, nunca puede existir estado en ellas (las relaciones no pueden contener atributos). El estado existe en los objetos solamente, y las relaciones no son objetos de primera clase.
La solución Java es convertir la relación en una entidad que contenga el estado deseado y mapear la nueva entidad a lo que anteriormente era la tabla de unión. La nueva entidad tendrá una relación de muchos a uno con cada uno de los tipos de entidad existentes, y cada uno de los tipos de entidad tendrá una relación de uno a muchos de vuelta a la nueva entidad que representa la relación.
La clave primaria de la nueva entidad será la combinación de las dos relaciones con los dos tipos de entidad. El siguiente código muestra todos los participantes en la relación entre Empleado y Proyecto.
classDiagram
class Empleado {
-int id
-String nombre
-long salario
}
class Proyecto {
-int id
-String nombre
}
class AsignacionProyecto {
-Date fechaInicio
}
Empleado o-- AsignacionProyecto
Proyecto o-- AsignacionProyecto
La relación entre Employee y Project
La relación entre Employee y Project es una relación de muchos a muchos, que se representa mediante una tabla de unión llamada EMP_PROJECT. Esta tabla contiene dos claves externas, EMP_ID y PROJECT_ID, que hacen referencia a las tablas EMPLOYEE y PROJECT, respectivamente. La clase ProjectAssignment representa la relación entre Employee y Project, y contiene un atributo adicional, startDate, que almacena la fecha en la que se realizó la asignación.
Mapeando el estado de la relación con una entidad intermedia:
Aquí tenemos una clave primaria compuesta que está compuesta por dos claves externas, una de cada una de las entidades que forman parte de la relación. La clase ProjectAssignmentId es la clave primaria compuesta y contiene los dos atributos que forman la clave primaria. La clase ProjectAssignment es la entidad que representa la relación entre Employee y Project. La relación entre Employee y Project se representa mediante una relación de muchos a uno con la entidad ProjectAssignment.
Nombre de los atributos de la IdClass
Los nombres de los atributos de la clase IdClass deben coincidir con los nombres de los atributos de la clase de la entidad que representa la relación. En este caso, los nombres de los atributos son employee y project.
Ojo, los tipos de datos deben coincidir con los tipos de datos de las claves de las entidades. En este caso, ambos atributos son de tipo int, que coincide con los tipos de datos de las claves de las entidades.
La clave primaria enteramente compuesta de la relación, con las dos columnas de clave externa que componen la clave primaria en la tabla de unión EMP_PROYECTO. La fecha en la que se realizó la asignación podría establecerse manualmente cuando se crea la asignación, o podría asociarse a un trigger que haga que se establezca cuando se crea la asignación en la base de datos. Ten en cuenta que, si se usara un trigger, entonces la entidad tendría que actualizarse desde la base de datos para poder poblar el campo de fecha de asignación en el objeto Java.
4.6 La anotación EmbeddedId (repaso)
@EmbeddedId es una alternativa a la anotación @IdClass.
Consideremos otro ejemplo en el que tenemos que persistir información de un Book, con titulo y idioma como los campos de clave primaria.
En este caso, la clase de clave primaria, IdLibro, debe estar anotada con @Embeddable:
Luego, necesitamos incrustar esta clase en la entidad Libro usando @EmbeddedId:
@EntitypublicclassLibro {
@EmbeddedIdprivate IdLibro bookId;
// constructores, otros campos, getters y setters}
4.7. @IdClass vs @EmbeddedId
Como podemos ver, la diferencia superficial entre estos dos es que con @IdClass tuvimos que especificar las columnas dos veces, una vez en IdCuenta y nuevamente en Cuenta; sin embargo, con @EmbeddedId no lo hicimos.
Sin embargo, hay algunas otras compensaciones.
Por ejemplo, estas estructuras diferentes afectan las consultas JPQL que escribimos.
Con @IdClass, la consulta es un poco más sencilla:
SELECT cuenta.numeroCuenta FROM Cuenta cuenta
Con @EmbeddedId, tenemos que hacer un recorrido extra:
SELECT libro.idLibro.titulo FROM Libro book
Además, @IdClass puede ser bastante útil en lugares donde estamos utilizando una clase de clave compuesta que no podemos modificar.
Si vamos a acceder a partes de la clave compuesta individualmente, podemos hacer uso de @IdClass, pero en lugares donde usamos frecuentemente el identificador completo como un objeto, se prefiere @EmbeddedId.
Ejercicio 7.3. Entidades principales de base de datos de películas.
El título de la película se guarda en el campo castelan.
El identificador de la película es entero (no autoincremento).
Los participantes de la película están relacionados por medio da de la tabla peliculapersonaxe, en la que el campo ocupacion identifica o tipo de ocupación de la película (‘Actor’, …):
Para empezar, crea las entidades Pelicula, Personaxe y Ocupacion.
A continuación, crea la entidad PeliculaPersonaxe que relaciona las entidades Pelicula y Personaxe y Ocupacion. Ten en cuenta que tiene un nuevo atributo personaxeInterpretado, que es el nombre del personaje interpretado por el actor en la película.
Mejora:
Crea las entidades asociadas a la base de datos. De modo que las columnas anoInicio, outrasDuracions, video, laserDisc pertenezca a una entidad DetallePelicula de tipo embebido.
Hay que tener en cuenta que estos elementos no siempre están presentes, por lo que deben ser opcionales.
Cuando hablamos de mapear colecciones hay tres tipos de objetos que pueden contener colecciones:
Entidades (@OneToMany o @ManyToMany).
Elementos embebidos (@ElementCollection)
Elementos básicos (@ElementCollection)
Sin embargo, las colecciones de elementos embebidos y básicos no se consideran relaciones. Son colecciones de elementos que se denominan colecciones de elementos, pues, a diferencia de las relaciones, que se relacionan con entidades independientes, las colecciones de elementos contienen objetos que dependen de la entidad que los referencia.
Sólo pueden ser obtenidos a través de la entidad que los contiene.
La anotación @ElementCollection se utiliza para mapear colecciones de elementos embebidos o básicos.
Dispones de dos elementos opcionales:
targetClass: de tipo Class indica la clase básica o embebida del elemento de la colección. Es opcional si el tipo de elementos de la colección se indica con genéricos, obligatorio en caso contrario.
fetch: de tipo FechType tipo de carga de los elementos de la colección, perezosa o ansiosa. Por defecto es perezosa.
En el siguiente ejemplo se requiere indicar la clase de los elementos de la colección con el atributo targetClass, pues es una colección sin genéricos (perezosa es el valor por defecto):
En este caso, la colección de teléfonos es una colección de elementos básicos. No es una relación, pues los teléfonos no son entidades independientes, sino que dependen de la persona que los referencia.
También podría ser un conjunto (Set) de elementos:
En el ejemplo anterior, la anotación @ElementCollection incluye el atributo targetClass que indica la clase de los elementos de la colección porque no se ha indicado el tipo de elementos que contiene la colección.
2. Tabla de colección: @CollectionTable
La anotación @CollectionTable se utiliza para especificar la tabla de la colección.
En el caso de las colecciones de elementos embebidos, los elementos no son entidades, por lo que no tendrá asociada una tabla, sin embargo, al no ser posible almacenar varios elementos en la misma columna, las colecciones de elementos embebidos y tipos básicos (algunos sistemas gestores de base de datos disponen de tipo de datos para ARRAY) requieren una tabla independiente denominada “Tabla de colección” que almacena los elementos de la colección.
En este caso, la tabla Vacaciones almacenará los elementos de la colección vacaciones de la entidad Persona. La tabla Vacaciones tendrá una columna idPersona que será la clave foránea que relacionará los elementos de la colección con la entidad Persona.
Si no se especifica la tabla de la colección, se utilizará una tabla por defecto con el nombre de la entidad y el nombre de la propiedad de la colección separados por un guion bajo. Por ejemplo, si no se especifica la tabla de la colección, la tabla por defecto de la colección vacaciones de la entidad Persona se llamará Persona_Vacaciones.
Por defecto, las columnas de la tabla de la colección tendrán el mismo nombre que la propiedad de la colección. Sin embargo, se pueden especificar las columnas de la tabla de colección con la anotación @Column.
Con colecciones de tipo básico, el nombre de a columna se obtiene de nombre de campo o propiedad de la colección.
Se puede sobrescribir con la anotación @Column indicando el nombre de la columna.
@ElementCollection// usa la tabla por defecto: Persona_telefonos@Column(name="telefono")
private List<String> telefonos;
Con colecciones de clases embebidas los nombres de las columnas se corresponden con los de las propiedades de la clase embebida.
Se puedes sobrescribir con la anotación @AttributeOverride o @AttributeOverrides indicando el nombre de la columna (si tiene referencias a otras entidades puede sobrescribirse con @AssociationOverride o @AssociationOverrides).
En el siguiente ejemplo completo se detalla cómo se pueden especificar las columnas de la tabla de colección con la anotación @Column:
@EmbeddablepublicclassDireccion {
protected String calle;
protected String ciudad;
protected String provincia;
// ...}
@EntitypublicclassPersona {
@Idprotected String numeroSeguridadSocial;
protected String nombre;
protected Direccion casa;
// ...@ElementCollection// usa la tabla por defecto: Persona_Alias@Column(name="nombre", length=50)
protected Set<String> alias =new HashSet();
// ...}
@EntitypublicclassMedicoextends Persona {
@ElementCollection@CollectionTable(name="Casa") // usa el nombre por defecto de la clave foránea.@AttributeOverrides({
@AttributeOverride(name="calle", column=@Column(name="calleCasa")),
@AttributeOverride(name="ciudad", column=@Column(name="ciudadCasa")),
@AttributeOverride(name="provincia", column=@Column(name="provinciaCasa"))
})
protected Set<Direccion> casas =new HashSet();
// ...}
En el ejemplo anterior hemos sobrescrito los nombres de las columnas de la tabla de colección Casa con la anotación @AttributeOverride y @AttributeOverrides.
3. Ordenación de colecciones
3.1. @OrderBy
La anotación @OrderBy se utiliza para ordenar los elementos de una colección. Se puede aplicar a colecciones de elementos básicos o embebidos, así como cualquier relación multivaluada (@OneToMany, @ManyToMany) de tipo List.
En este caso, la colección vacaciones se ordenará por la fecha de inicio de las vacaciones en orden descendente.
El elemento de la anotación @OrderBy es una lista de nombres de propiedades separados por comas, que se utilizan para ordenar los elementos de la colección.
La ordenación puede realizarse para cualquier relación multivaluada de tipo List:
@EntitypublicclassCurso {
// ...@ManyToMany@OrderBy("apelidos ASC")
public List<Estudante>getEstudantes() {
//... }
// ...}
@EntitypublicclassEstudante {
// ...@ManyToMany(mappedBy="estudantes")
@OrderBy// ordena por clave primariapublic List<Curso>getCurso() {
//... }
// ...}
3.2. @OrderColumn
Otro tipo de ordenación es por medio de la anotación @OrderColumn que se utiliza para ordenar los elementos de una colección. Se puede aplicar a colecciones de elementos básicos o embebidos, así como cualquier relación multivaluada (@OneToMany, @ManyToMany) de tipo List.
El uso de @OrderColumn es incompatible con @OrderBy (uno u otro, no ambos).
4. Generación de claves primarias para colecciones de elementos: @CollectionId (Hibernate) (*)
A modo de curiosidad, se puede mencionar que existe una anotación @CollectionId que se utiliza para generar claves primarias para las colecciones de elementos en Hibernate (no en JPA, por lo que este apartado no es relevante para el examen).
La anotación @CollectionId es exclusiva de Hibernate se utiliza para generar claves primarias para las colecciones de elementos. Se puede aplicar a colecciones de elementos básicos o embebidos.
En este caso, la entidad Persona tiene una colección de fechas de vacaciones y una colección de direcciones. La tabla Vacaciones almacenará las fechas de vacaciones y la tabla Direccion almacenará las direcciones de la entidad Persona.
En este caso, la colección estudiantes se ordenará por el apellido de los estudiantes en orden ascendente.
@EntitypublicclassEstudiante {
// ...@ManyToMany(mappedBy="estudiantes")
@OrderBy// ordena por clave primariapublic List<Curso>getCurso() {
//...// }
// ...}
6. One-to-many vs @ElementCollection
Anotaciones One to Many
Necesitamos usar anotaciones One to Many si creamos una relación entre dos entidades (TABLAS).
Podemos representar Tienda, la tienda tiene muchas sucursales, y ambas son una entidad, lo que significa que ambas tienen una tabla dentro de nuestra base de datos.
Después de ejecutar el código, el proveedor de persistencia (Hibernate,..) creará dos tablas dentro de tu base de datos, la primera llamada tienda y la segunda llamada sucursal.
Ahora necesitamos crear un repositorio o una clase DAO para ambas entidades para insertar y obtener los datos de la base de datos.
Si usamos String Boot, podemos usar @Repository para crear un componente Repositorio (lo veremos más adelante).
Podemos usar @Repository para crear un componente Repositorio.
Y después de eso, tenemos una nueva relación entre Tienda y Sucursal.
Anotación ElementCollection
@ElementCollection es una anotación JPA estándar (anteriormente con Hibernate se empleaba la anotación propietaria CollectionOfElements).
Significa que la colección no es una colección de entidades, sino una colección de tipos simples (cadenas, etc.) o una colección de elementos integrables/embebidos (clase anotada con @Embeddable).
También significa que los elementos son propiedad completamente de las entidades que los contienen: se modifican cuando se modifica la entidad, se eliminan cuando se elimina la entidad, etc. No pueden tener su propio ciclo de vida.
Ahora veremos cómo podemos implementar con colección de elementos.
La anotación más simple, @ElementCollection, le dice al compilador que estamos asignando una colección, en la que @CollectionTable proporciona el nombre de la tabla objetivo, y luego @JoinColumn especifica la columna real que unimos como se muestra a continuación:
Del último ejemplo, ahora tenemos dos entidades Tienda y Sucursal, y la relación entre ellas, y añadiremos una nueva clase llamada Producto.
Producto.java:
@EmbeddablepublicclassProducto {
// no necesitamos usar id porque no es una entidad@Column(name="nome")
private String nome;
@Column(name="precio")
private Double precio;
}
Y ahora añadimos una nueva anotación Elementcollection en la clase Tienda y un objeto producto de la clase Producto.
Y ahora después de crearlo la tabla de Producto tiene idTienda como clave primaria. Pero es una clave foránea de la tabla de tienda y no una clave primaria para la tabla de productos.
El objeto Embeddable no tiene un Repositorio/DAO porque depende de la entidad, por ejemplo, el producto embebido depende de la entidad de la Tienda. Y podemos insertarlo sin usar el Repositorio de la Tienda.
La diferencia entre ambos enfoques:
Entidad:
Cuando creamos una entidad, podemos relacionarla con el repositorio/DAO JPA para escribir nuestra consulta o usar la consulta integrada de JPA.
Embeddable:
Cuando creamos una clase integrable, debe relacionarse con cualquier clase de entidad, porque depende de la clase de entidad. y no podemos crear un repositorio.
Resumen
@ElementCollection, por otro lado, es muy similar a @OneToMany excepto que el objeto objetivo no es una entidad.
Con @ElementCollection, no podemos consultar, persistir o fusionar (merge) objetos objetivo de forma independiente de su objeto principal.
No admite operaciones de cascada. Esto significa que los objetos objetivo siempre se persisten, se fusionan o se eliminan junto con su objeto principal.
@ElementCollection es una forma fácil de definir una colección con objetos simples/básicos. @OneToMany es el mejor para casos de uso complejos en los que se requiere un control detallado.
Las consultas JPA son consultas que se realizan sobre las entidades de la base de datos. Estas consultas se pueden realizar de varias formas:
a. Lenguajes de consulta:
Consultas JPQL: (Jakarta Persistence Query Language): lenguaje de consulta independiente de bases de datos, orientado a objetos que operqa sobre el modelo de entidades lógico (o sobre el modelo físico de la base de datos). Las consultas JPQL se realizan sobre las entidades de la base de datos y no sobre las tablas de la base de datos. Ejemplo:
TypedQuery<Empleado> q = em.createQuery("SELECT e FROM Empleado e WHERE e.nome = :nome", Empleado.class);
q.setParameter("nome", "Otto");
List<Empleado> resultado = q.getResultList();
Consultas nativas SQL: consultas que se realizan sobre la base de datos, utilizando el lenguaje de consulta de la base de datos (SQL). Ejemplo:
Query q = em.createNativeQuery("SELECT * FROM EMPLEADO WHERE NOME = ?1", Empleado.class);
q.setParameter(1, "Otto");
List<Empleado> resultado = q.getResultList();
b. API Criteria: API que se utiliza para construir consultas de forma programática. Esta API se usa para construir consultas basadas en objetos Java y no en cadenas/String de consulta. Ejemplo:
Crea una instancia de TypedQuery de JPQL. La lista de elementos del SELECT debe tener un único elemento, que debe poder asignarse al tipo pasado como argumento, claseResultado.
Crea una instancia de Query para ejecutar una declaración SQL nativa, por ejemplo, para actualizar o eliminar. Si la consulta no es de borrado/actualización, devuelve un array de objetos (Object[]).
Query createNativeQuery(String sqlString, Class claseResultado)
Crea una instancia de Query para ejecutar una consulta SQL nativa, indicando el tipo de datos revuelto.
Ejecuta la consulta y devuelve un java.util.stream.Streamno tipado de resultados. El Stream de resultados no es tipada. Por defecto delega al método getResultList().stream(), sin embargo el proveedor de persistencia, Hibernate…, puede sobrescribirlo para mejorar rendimiento y funcionalidades.
La interfaz jakarta.persistence.TypedQuery<X> sobrescribe los métodos List<X> getResultList(), default Stream<X> getResultStream() y X getSingleResult() para devolver una lista de elementos de tipo X, un Stream de tipo X y un elemento de tipo X, respectivamente.
El método getSingleResult() lanza una excepción de tipo NoResultException si no se encuentran resultados.
Ejemplo completo de consulta JPA
import java.util.Scanner;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Persistence;
import java.util.List;
publicclassJPAQuery {
publicstatic Scanner SCAN =new Scanner(System.in);
publicstaticvoidmain(String[] args) {
EntityManager em;
if (args.length!= 1) {
// em = JPAUtil.getEntityManager(); em = Persistence.createEntityManagerFactory("bibliotecaH2").createEntityManager();
} else {
// em = JPAUtil.getEntityManager(args[0]); em = Persistence.createEntityManagerFactory(args[0]).createEntityManager();
}
System.out.println("Escribe la orden \"salir;\" para salir.");
boolean salir =false;
while (!salir) {
System.out.print("Jakarta Persistence QL> ");
StringBuilder sb =new StringBuilder();
do {
sb.append(" ").append(SCAN.nextLine().trim());
} while (!sb.toString().endsWith(";"));
String consulta = sb.substring(0, sb.length() - 1);
if (!consulta.equalsIgnoreCase("salir")) { //if (consulta.isEmpty()) {
continue;
}
try {
if ("select".equalsIgnoreCase(consulta.trim().substring(0, 6))) {
// Consulta JPQL. La interfaz TypedQuery hereda de Query// y permite la ejecución de consultas JPQL con la devolución de// resultados tipados.// TypedQuery<?> q = em.createQuery(consulta, Object.class); List<?> resultado = em.createQuery(consulta).getResultList(); // Las wildcard permiten devolver cualquier tipo de objetoif (!resultado.isEmpty()) {
int count = 0;
for (Object o : resultado) {
System.out.print(++count +" ");
mostrarResultados(o);
}
} else {
System.out.println("0 resultados de la consulta");
}
} else {
int i = em.createQuery(consulta).executeUpdate();
System.out.println(i +" elementos modificados");
}
} catch (Exception e) {
System.out.println("Error al procesar la consulta: "+ e.getMessage());
}
} else {
salir =true;
}
}
}
privatestaticvoidmostrarResultados(Object resultado) {
if (resultado ==null) {
System.out.print("NULL");
} elseif (resultado instanceof Object[] fila) {
System.out.print("[");
for (Object o : fila) {
mostrarResultados(o);
}
System.out.print("]");
} elseif (resultado instanceof Long || resultado instanceof Double || resultado instanceof String) {
System.out.print(resultado.getClass().getName() +": "+ resultado);
} else {
// ReflectionToStringBuilder es una clase de Apache Commons Lang que// permite la conversión de objetos a cadenas de texto.// System.out.print(ReflectionToStringBuilder.toString(resultado, ToStringStyle.SHORT_PREFIX_STYLE)); System.out.print(resultado);
}
System.out.println();
}
}
Ejercicio 09.01. Ejecución de consultas JPA Películas
Ejercicio. Consultas.
Implementa el ejemplo anterior contra una base de datos de películas proporcionada.
URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas
URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false
Pese a todo lo mejor es que el usuario y contraseña se guarden en un archivo de propiedades, persistence.xml, como se ha visto en el tema de configuración.
Realiza las siguientes consultas:
Las películas que no tienen año de inicio definido:
SELECT p.castelan, p.anoFin, p.anoInicio
FROM Pelicula p
WHERE p.anoInicio ISNOTNULL;
Las películas con una duración superior a 120 minutos:
SELECT p.castelan, p.anoFin, p.duracion
FROM Pelicula p
WHERE p.duracion >120;
Las películas con Antonio Banderas:
SELECT p FROM Pelicula p JOIN p.personaxes pp JOIN pp.personaxe per WHERE per.nomeOrdenado LIKE'Antonio Banderas';
Las tablas de la base de datos sobre las que hay que implantar las entidades y realizar las consultas son las del ejercicio anterior.
2. Jakarta Persistence Query Language (JPQL)
2.1. Historia de JPQL
EL origen de JPQL es Enterprise JavaBeans Query Language (EJB QL), que se introdujo en la especificación EJB 2.0 para permitir a los desarrolladores escribir métodos portables de búsqueda y selección para beans de entidad gestionados por contenedores. Se basaba en un pequeño subconjunto de SQL e introdujo una forma de navegar a través de las relaciones de entidad tanto para seleccionar datos como para filtrar los resultados. Sin embargo, imponía limitaciones estrictas en la estructura de la consulta, limitando los resultados a una única entidad o a un campo persistente de una entidad. Aunque eran posibles las uniones internas entre entidades, se utilizaba una notación extraña. La versión inicial ni siquiera admitía la ordenación.
La especificación EJB 2.1 ajustó EJB QL, añadiendo soporte para la ordenación e introduciendo funciones agregadas básicas; pero nuevamente, la limitación de un único tipo de resultado obstaculizaba el uso de agregados. Se podía filtrar los datos, pero no había equivalente a las expresiones GROUP BY y HAVING de SQL.
Jakarta Persistence QL extiende significativamente EJB QL, eliminando muchas debilidades de las versiones anteriores mientras preserva la compatibilidad hacia atrás. Algunas de las características disponibles añadidas:
Tipos de resultados de valor único y múltiple: las consultas JPQL pueden devolver una única entidad, un campo persistente de una entidad, una lista de entidades o una lista de campos persistentes.
Funciones agregadas, con cláusulas de ordenación y agrupación: GROUP BY y HAVING.
Una sintaxis de unión más natural, incluyendo soporte para inner joins y outer joins: LEFT JOIN y RIGHT JOIN.
Expresiones condicionales que involucran subconsultas: EXISTS, ALL, ANY y SOME.
Consultas de actualización y eliminación para cambios masivos de datos: UPDATE y DELETE.
Proyección de resultados en clases no persistentes: SELECT NEW (ideal para DTOs, Data Transfer Objects).
2.2. Sintaxis de JPQL
La sintaxis de JPQL es similar a la de SQL, pero opera sobre entidades y sus atributos en lugar de tablas y columnas.
Las consultas JPQL se definen como cadenas de texto y se pueden incrustar en el código Java.
Las consultas JPQL se pueden ejecutar de forma dinámica en tiempo de ejecución, lo que permite a las aplicaciones adaptar las consultas a las condiciones cambiantes.
2.2.1. Consultas SELECT
Las consultas SELECT de JPQL se utilizan para recuperar datos de la base de datos. La sintaxis básica de una consulta SELECT es:
Una declaración SELECT es una cadena que consta de las siguientes cláusulas:
Una cláusula SELECT, que determina el tipo de objetos o valores que se seleccionarán.
Una cláusula FROM, que proporciona declaraciones que designan el dominio al cual se aplican las expresiones especificadas en las otras cláusulas de la consulta.
Una cláusula WHERE opcional, que se puede utilizar para restringir los resultados que devuelve la consulta.
Una cláusula GROUP BY opcional, que permite agregar los resultados de la consulta en términos de grupos.
Una cláusula HAVING opcional, que permite filtrar sobre grupos agregados.
Una cláusula ORDER BY opcional, que se puede utilizar para ordenar los resultados que devuelve la consulta.
En la sintaxis de BNF, una declaración SELECT se define como:
Una declaración SELECT siempre debe tener una cláusula SELECT y una cláusula FROM. Los corchetes cuadrados [] indican que las otras cláusulas son opcionales.
La consulta más simple en Jakarta Persistence QL selecciona todas las instancias de un solo tipo de entidad:
SELECT e
FROM Empleado e
Jakarta Persistence QL utiliza la sintaxis de SQL.
La principal diferencia entre SQL y Jakarta Persistence QL para esta consulta es que, en lugar de seleccionar de una tabla, se ha especificado una entidad del modelo de dominio de la aplicación. La cláusula SELECT de la consulta también es ligeramente diferente, enumerando solo el alias de Empleado e. Esto indica que el tipo de resultado de la consulta es la entidad Empleado, por lo que ejecutar esta instrucción dará como resultado una lista de cero o más instancias de Empleado.
Con un alias se puede navegar a través de las relaciones de entidad utilizando el operador punto (.):
SELECT e.nombre
FROM Empleado e
El campo persistente (e.nombre, en este caso) de la entidad puede ser de tipo simple o incrustable, o a una asociación que conduce a otra entidad o colección de entidades. Dado que la entidad Empleado tiene un campo persistente llamado nombre de tipo String, esta consulta dará como resultado una lista de cero o más objetos String.
También se puede seleccionar una entidad que ni siquiera mencionamos en la cláusula FROM. Consideremos el siguiente ejemplo:
SELECT e.departamento
FROM Empleado e
Un Empleado tiene una relación de muchos a uno con su Departamento llamada departamento, por lo que el tipo de resultado de la consulta es la entidad Departamento.
2.2.2. Filtrado de resultados
Para filtrar resultados se utiliza la cláusula WHERE.
La mayoría de operaciones disponibles en SQL están disponibles en JPQL, como:
Operadores básicos de comparación: =, >, <, >=, <=, <>.
Expresiones: BETWEEN, LIKE, IN, IS NULL, IS NOT NULL.
Funciones de cadena: CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE.
Funciones de fecha: CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, EXTRACT, …
Funciones de agregado: AVG, COUNT, MAX, MIN, SUM.
Por ejemplo:
SELECT e
FROM Empleado e
WHERE e.salario >1000
En este caso, la cláusula WHERE se utiliza para filtrar los resultados de la consulta. La condición e.salario > 1000 se evalúa para cada instancia de Empleado en la base de datos, y solo las instancias que satisfacen la condición se incluyen en el resultado de la consulta.
SELECT e
FROM Empleado e
WHERE e.departamento.nombre ='Ventas'AND e.direccion.ciudad ='Santiago'
2.2.3. Proyección de resultados
En aquellos casos en los que se encuentre interesado en recuperar solo algunos campos de una entidad, se puede utilizar la cláusula SELECT para proyectar los resultados. Por ejemplo:
SELECT e.nombre, e.salario
FROM Empleado e
WHERE e.salario >1000
Dependiendo de cómo una entidad está mapeada en la base de datos, puede ser más eficiente recuperar solo algunos campos de una entidad que recuperar la entidad completa. En este caso, la consulta devuelve una lista de objetos Object[], donde cada objeto es un array de dos elementos que contiene el nombre y el salario de un empleado.
Referencias de constructores NEW
Las referencias de constructores son una buena opción para casos de uso de ==solo lectura=. Son más cómodos de usar que las proyecciones de valores escalares y evitan los gastos generales de las entidades administradas.
JPQL le permite definir una llamada al constructor en la cláusula SELECT. Sólo se necesita proporcionar el nombre de clase completo y especificar los parámetros del constructor de un constructor existente. De manera similar a la proyección de entidad, genera una consulta SQL que devuelve las columnas de la base de datos requeridas y utiliza la referencia del constructor para crear una instancia de un nuevo objeto para cada registro en el conjunto de resultados.
SELECTnew com.javhoz.ad.jpa.AutorDTO(a.idAutor, a.nome, a.apelidos) FROM Autor a
Resultados de consulta distintos
El operador DISTINCT de SQL que elimina duplicados de una proyección también lo admite JPQL:
SELECTDISTINCT e.departamento
FROM Empleado e
Expresiones condicionales con CASE
Las expresiones condicionales se pueden utilizar en la cláusula WHERE para filtrar los resultados de la consulta. Por ejemplo:
SELECT e
FROM Empleado e
WHERE e.salario >1000AND e.salario <2000
Sin embargo, JPQL admite expresiones CASE: case general, case simple y case de búsqueda.
SELECT e.nombre,
CASEWHEN e.salario >2000THEN'Alto'ELSE'Bajo'ENDFROM Empleado e
O también para valores concretos:
SELECT e.nombre,
CASE e.salario
WHEN1000THEN'Bajo'WHEN2000THEN'Medio'ELSE'Alto'ENDFROM Empleado e
Ejemplos:
UPDATE Empleado e
SET e.salario =CASEWHEN e.clasificacion =1THEN e.salario *1.1WHEN e.clasificacion =2THEN e.salario *1.05ELSE e.salario *1.01ENDUPDATE Empleado e
SET e.salario =CASE e.clasificacion WHEN1THEN e.salario *1.1WHEN2THEN e.salario *1.05ELSE e.salario *1.01ENDSELECT e.nombre,
CASETYPE(e) WHEN Desarrollador THEN'Desarrollador'WHEN Administrador THEN'Administrador'WHEN Profesor THEN'Profesor'ELSE'Empleado'ENDFROM Empleado e
WHERE e.departamento.nombre ='Sistemas'SELECT e.nombre,
f.nombre,
CONCAT(CASEWHEN f.kmAnuales >50000THEN'Platinum 'WHEN f.kmAnuales >25000THEN'Dorada 'WHEN f.kmAnuales >10000THEN'Plateada 'ELSE''END,
' Frecuencia')
FROM Empleado e JOIN e.planDeViaje f
2.2.4. Joins entre entidades
El tipo de resultado de una consulta SELECT no puede ser una colección; debe ser un objeto de valor único, como una instancia de entidad o un tipo de campo persistente.
Expresiones como e.telefonos son ilegales en la cláusula SELECT porque darían como resultado instancias de Collection (cada ocurrencia de e.telefonos es una colección, no una instancia). Por lo tanto, al igual que con SQL y tablas, si queremos navegar a lo largo de una asociación de colección y devolver elementos de esa colección, debemos hacer un join las dos entidades.
Por ejemplo:
SELECT p.numero
FROM Empleado e, Telefono t
WHERE e = t.empleado AND e.departamento.nombre ='Desarrollo'AND t.tipo ='Móvil'
También se pueden expresar en la cláusula FROM utilizando el operador JOIN. La ventaja de este operador es que el join se puede expresar en términos de la propia asociación, y el motor de consulta suministrará automáticamente los criterios de unión necesarios cuando genere el SQL. La consulta anterior se puede reescribir para usar el operador JOIN. Recuerda que el alias p es de tipo Telefono:
SELECT p.numero
FROM Empleado e JOIN e.telefonos p
WHERE e.departamento.nombre ='Desarrollo'AND p.tipo ='Móvil'
Jakarta Persistence QL admite varios tipos de uniones, incluidas uniones internas y externas, así como una técnica llamada fetch joins para cargar de manera proactiva datos asociados con el tipo de resultado de una consulta pero que no se devuelven directamente.
Inner Joins (Joins relacionados)
Un Inner Join devuelve solo las filas que tienen correspondencias en ambas tablas. En Jakarta Persistence QL, un join interno se expresa con la palabra clave [INNER] JOIN:
La definición de la entidad Autor proporciona toda la información que se necesita para unirla a la entidad Libro, y no es necesario proporcionar una declaración ON adicional.
La palabra INNER es opcional:
SELECTcFROM Cliente cINNERJOINc.pedidos p WHEREc.estado =1
Es equivalente a la consulta con el constructor IN:
SELECTOBJECT(c) FROM Cliente c, IN(c.pedidos) p WHEREc.estado =1
La siguiente consulta se une a Empleado, Información de contacto y Teléfono. ContactoInfo es una clase embedded que consta de una dirección y un conjunto de teléfonos. El teléfono es una entidad.
SELECT t.compañia
FROM Empleado e JOIN e.contactoInfo cJOINc.telefonos t
WHEREc.direccion.codigoPostal ='15705'
Se puede especificar una condición de unión para una unión interna. Esto equivale a la especificación de la misma condición en la cláusula WHERE.
Left Outer Joins
LEFT JOIN y LEFT OUTER JOIN son sinónimos.
A veces sólo quieres unirte a las entidades relacionadas que cumplen condiciones adicionales.
Este Join perimte recibir un conjunto de entidades cuyos valores asociados a la condición de unión pueden ser nulos.
Sintaxis:
LEFT [OUTER] JOIN join_association_path_expression [AS] identification_variable [join_condition]
Si no se especifica la condición de unión, el join se realiza en función de la relación de asociación.
SELECT s.nombre, COUNT(p)
FROM Proveedor s LEFTJOIN s.productos p
GROUPBY s.nombre
Equivalente a SQL:
SELECT s.nombre, COUNT(p.id)
FROM Proveedor s LEFTJOIN Procucto p
ON s.idProveedor = p.idProducto
GROUPBy s.nombre
Puede añadirse otra condición a la unión explícita:
SELECT s.nombre, COUNT(p)
FROM Proveedor s LEFTJOIN s.productos p
ON p.estado ='stock'GROUPBY s.nombre
Equivalente a SQL:
SELECT s.nombre, COUNT(p.id)
FROM Proveedor s LEFTJOIN Producto p
ON s.idProveedor = p.idProducto
AND p.estado ='stock'GROUPBy s.nombre
La asociación referenciada por el lado derecho de la cláusula FETCH JOIN debe ser una asociación o colección de elementos que se referencia desde una entidad o integrable que se devuelve como resultado de la consulta.
No se permite especificar una variable de identificación para los objetos referenciados por el lado derecho de la cláusula FETCH JOIN y, por lo tanto, las referencias a las entidades o elementos obtenidos de manera implícita no pueden aparecer en ninguna otra parte de la consulta.
La siguiente consulta devuelve un conjunto de departamentos. Como efecto secundario, también se recuperan los empleados asociados a esos departamentos, aunque no formen parte del resultado explícito de la consulta. La inicialización del estado persistente o de los campos o propiedades de relación de los objetos que se recuperan como resultado de un fetch join está determinada por los metadatos de esa clase, en este ejemplo, la clase de entidad Empleado.
SELECT d
FROM Departamento d LEFTJOINFETCH d.empleados
WHERE d.numeroDepartamento =1
Un fetch join tiene las mismas semánticas de unión que la unión interna u externa correspondiente, excepto que los objetos relacionados especificados en el lado derecho de la operación de unión no se devuelven en el resultado de la consulta ni se hacen referencia de ninguna otra manera en la consulta. Por lo tanto, por ejemplo, si el departamento 1 tiene cinco empleados, la consulta anterior devuelve cinco referencias a la entidad del departamento 1.
2.2.5. Consultas Agregadas
La sintaxis para consultas agregadas en Jakarta Persistence QL es muy similar a la de SQL. Hay cinco funciones agregadas admitidas:
AVG.
COUNT.
MIN.
MAX.
SUM.
Los resultados pueden agruparse en la cláusula GROUP BY y filtrarse mediante la cláusula HAVING. Nuevamente, la diferencia está en el uso de expresiones de entidad al especificar los datos que se van a agregar. Por ejemplo:
SELECT d, COUNT(e), MAX(e.salario), AVG(e.salario)
FROM Departamento d JOIN d.empleados e
GROUPBY d
HAVINGCOUNT(e) >=5
2.2.6. Parámetros en las consultas
Jakarta Persistence QL admite dos tipos de sintaxis para la vinculación de parámetros:
Vinculación posicional, donde los parámetros se indican en la cadena de consulta mediante un signo de interrogación seguido del número de parámetro (similar a JDBC):
SELECT e
FROM Empleado e
WHERE e.departamento =?1ANDe.salario >?2
Parámetros con nombre, que se indican en la cadena de consulta mediante dos puntos seguidos del nombre del parámetro:
SELECT e
FROM Empleado e
WHERE e.departamento = :dept ANDe.salario > :base
Ejercicio 09.02. Creación de consultas JPA Películas
a) Obtener todas las películas que tienen una duración mayor a 120 minutos.
b) Obtener todas las películas que pertenecen a un género específico (por ejemplo, “Drama”).
c) Obtener todas las ocupaciones que tienen más de 5 películas asociadas.
d) Obtener todas las películas que tienen un país específico (por ejemplo, “España”).
e) Obtener todas las películas que tienen al menos un personaje interpretado por un actor de un país específico (por ejemplo, “Francia”).
f) Obtener todas las películas que tienen música compuesta por un compositor específico (por ejemplo, “John Williams”).
g) Obtener todas las películas que tienen un personaje interpretado por un actor con un nombre específico (por ejemplo, “Tom Hanks”).
h) Obtener todas las películas que tienen un género específico y que fueron producidas en un año específico (por ejemplo, “Acción” y 2005).
i) Obtener todas las películas que tienen un personaje interpretado por un actor de un género específico (por ejemplo, “Mujer”).
f) Obtener todas las películas que tienen un personaje interpretado por un actor que nació en un país específico y que tienen una duración mayor a 100 minutos.
g) Devolver todos los países que no tienen películas asociadas, puedes usar una consulta JPQL que utilice una subconsulta o un LEFT JOIN con una condición IS NULL.
Ejercicio 09.03. Consultas SQL a JPQL (películas)
Ejercicio. Consultas sobre la base de datos de películas
Amplía el ejercicio anterior la base de datos de películas proporcionada.
URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas
URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false
Pese a todo lo mejor es que el usuario y contraseña se guarden en un archivo de propiedades, persistence.xml, como se ha visto en el tema de configuración.
Las tablas de la base de datos sobre las que hay que implantar las entidades y realizar las consultas son las del ejercicio anterior.
Realiza las siguientes consultas:
Muestra la película solicitando el id:
SELECT castelan, orixinal, anoFin, poster ISNOTNULLas tenPoster
FROM pelicula WHERE idPelicula = :identificador
Muestra las películas que tienen algún personaje (IS EMPTY) o no tienen personajes (IS NOT EMPTY).
Muestra las películas que tienen personajes con una ocupación concreta:
SELECT P.nome FROM peliculapersonaxe PP, personaxe P
WHERE P.idPersonaxe=PP.idPersonaxe ANDPP.ocupacion='OCUPACIÓNCONCRETA'AND PP.idPelicula=IDENTIFICADOR_PELICULA
Muestra los títulos de las películas en las que ha trabajado un actor concreto.
Listar el número de películas de acuerdo con el nombre propocionado:
(Crea una clase PeliculaDTO con los campos idPelicula, castelan, orixinal, anoFin, tenPoster (booleano) y realiza la consulta)
SELECT idPelicula, castelan, orixinal, anoFin, poster ISNOTNULLas tenPoster
FROM pelicula WHERE castelan LIKE‘%:nombre%’ORDERBY5DESC, castelan ASC
Consulta los datos de las ocupaciones de los personajes de una película:
SELECT O.ocupacion FROM ocupacion O WHEREEXISTS (
SELECT idPelicula FROM peliculapersonaxe PP WHEREO.ocupacion=PP.ocupacion
AND PP.idPelicula=IDENTIFICADOR_DE_PELICULA)
AND O.orde<>0ORDERBY O.orde
y los nombres sde los personajes que tienen esa ocupación:
SELECT P.nome FROM peliculapersonaxe PP, personaxe P
WHERE P.idPersonaxe=PP.idPersonaxe ANDPP.ocupacion='OCUPACIÓNCONCRETA'AND PP.idPelicula=IDENTIFICADOR_PELICULA
3. Definición de Consultas
Jakarta Persistence proporciona las interfaces Query y TypedQuery para configurar y ejecutar consultas.
La interfaz Query se utiliza en casos en los que el tipo de resultado es Object o en consultas dinámicas cuando el tipo de resultado puede no ser conocido de antemano.
La interfaz TypedQuery es la preferida y se puede utilizar siempre que se conozca el tipo de resultado.
TypedQuery hereda de Query, por lo que una consulta fuertemente tipada siempre se puede tratar como una versión no tipada, aunque no al revés.
Una implementación de la interfaz adecuada para una consulta dada se obtiene a través de uno de los métodos Factory ("createQuery, createNamedQuery, createNativeQuery, createStoredProcedureQuery, getCriteriaBuilder().createQuery") en la interfaz EntityManager (pueden verse en la tabla anterior). La elección del método Factory depende del tipo de consulta (Jakarta Persistence QL, SQL u objeto de criterios), si la consulta ha sido predefinida y si se desean resultados fuertemente tipados (en la tabla anterior pueden verse ejemplos).
Hay tres enfoques para definir una consulta Jakarta Persistence QL:
Una consulta puede ser creada dinámicamente en tiempo de ejecución:
Las consultas dinámicas de Jakarta Persistence QL más que cadenas y, por lo tanto, pueden definirse sobre la marcha según sea necesario.
Query q = em.createQuery("SELECT e FROM Empleado e WHERE e.departamento = :dept AND e.salario > :base");
q.setParameter("dept", "Desarrollo");
q.setParameter("base", 1000);
List<Empleado> resultado = q.getResultList()
Configurada en los metadatos de la unidad de persistencia (por medio de una anotación o XML) y posteriormente ser referenciada por nombre:
Las consultas con nombre son estáticas e inalterables, pero son más eficientes de ejecutar porque el proveedor de persistencia puede traducir la cadena de Jakarta Persistence QL a SQL una vez cuando la aplicación comienza, en lugar de cada vez que se ejecuta la consulta.
@NamedQuery(name="Empleado.findByDeptAndSalario", query="SELECT e FROM Empleado e WHERE e.departamento = :dept AND e.salario > :base")
Especificada dinámicamente y guardada para ser referenciada más adelante por nombre.
Definir dinámicamente una consulta y luego darle un nombre permite que una consulta dinámica se reutilice varias veces a lo largo de la vida de la aplicación, pero incurre en el costo de procesamiento dinámico solo una vez.
3.1. Definición Dinámica de Consultas
Una consulta puede definirse dinámicamente pasando la cadena de consulta Jakarta Persistence QL y el tipo de resultado esperado al método createQuery() de la interfaz EntityManager (el tipo de resultado puede omitirse para crear una consulta no tipada).
Se admiten todos los tipos de consultas Jakarta Persistence QL y uso de parámetros. La capacidad de construir una cadena en tiempo de ejecución y usarla para definir una consulta es útil, especialmente para aplicaciones donde el usuario puede especificar criterios complejos y la forma exacta de la consulta no puede conocerse de antemano.
Jakarta Persistence también admite una API Criteria para crear consultas dinámicas mediante objetos Java.
Un problema a considerar con las consultas de cadena dinámicas es el costo de traducir la cadena de Jakarta Persistence QL a SQL para su ejecución. Un motor de consulta típico tendrá que analizar la cadena de Jakarta Persistence QL en un árbol de sintaxis, obtener los metadatos de asignación objeto-relacional para cada entidad en cada expresión y luego generar el SQL equivalente. Para aplicaciones que emiten muchas consultas, el costo de rendimiento del procesamiento dinámico de consultas puede convertirse en un problema.
Muchos motores de consultas almacenarán en caché el SQL traducido para su uso posterior, pero esto se puede vencer fácilmente si la aplicación no utiliza la vinculación de parámetros y concatena los valores de parámetros directamente en las cadenas de consulta. Esto tiene el efecto de generar una consulta nueva y única cada vez que se construye una consulta que requiere parámetros.
Ejemplo, que busca información salarial dado el nombre de un departamento y el nombre de un empleado, nada recomendable:
publicclassServicioConsulta {
@PersistenceContext(unitName="ConsultasDinamicas") /* Se usa cuando se inyecta el EntityManager de la unidad de persistencia "UnidadConsultas" y es gestionado por el contenedor (no para aplicaciones Java SE). */ EntityManager em;
publiclongconsultarSalarioEmpleado(String nombreDepartamento, String nombreEmpleado) {
String consulta ="SELECT e.salario "+"FROM Empleado e "+"WHERE e.departamento.nombre = '"+ nombreDepartamento +"' AND "+" e.nombre = '"+ nombreEmpleado +"'";
return em.createQuery(consulta, Long.class).getSingleResult();
}
}
. Hay dos problemas: uno relacionado con el rendimiento y otro relacionado con la seguridad:
Como los nombres se concatenan en la cadena (en lugar de usar la vinculación de parámetros), efectivamente se crea una consulta nueva y única cada vez. Cien llamadas a este método podrían generar potencialmente cien cadenas de consulta diferentes.
En este ejemplo es que es vulnerable a ataques de inyección SQL, donde un usuario malintencionado podría pasar un valor que altera la consulta (por ejemplo, el gerente del departamento está consultando los salarios de sus empleados). Si el nombre fuera el texto “CALQUERA’ OR ‘Pepe’ = ‘Pepe”, la consulta real analizada por el motor de consultas sería la siguiente:
SELECT e.salario
FROM Empleado e
WHERE e.departamento.nombre ='Desarrollo'AND e.nombre ='CALQUERA'OR'Pepe'='Pepe'
Al introducir la condición OR, el usuario se ha dado acceso efectivo al valor salarial de cualquier empleado, porque la condición AND original tiene una precedencia más alta que OR.
El uso de parámetros con nombre reduce la cantidad de consultas únicas analizadas por el motor de consultas y elimina la posibilidad de inyección SQL:
publicclassServicioConsulta {
privatestaticfinal String CONSULTA ="SELECT e.salario "+"FROM Empleado e "+"WHERE e.departamento.nombre = :nombreDepartamento AND "+" e.nombre = :nombreEmpleado ";
@PersistenceContext(unitName="UnidadConsultas") // Se usa cuando se inyecta el EntityManager de la unidad de persistencia "UnidadConsultas" y es gestionado por el contenedor (no para aplicaciones Java SE). EntityManager em; // Cuando se inyecta el EntityManager, se inyecta el EntityManager de la unidad de persistencia "UnidadConsultas".publiclongconsultarSalarioEmpleado(String nombreDepartamento, String nombreEmpleado) {
return em.createQuery(CONSULTA, Long.class)
.setParameter("nombreDepartamento", nombreDepartamento)
.setParameter("nombreEmpleado", nombreEmpleado)
.getSingleResult();
}
}
Los parámetros se empaquetan utilizando la API de JDBC y son manejados directamente por la base de datos. El texto de una cadena de parámetros se cita efectivamente por la base de datos, por lo que el ataque malicioso realmente produciría la siguiente consulta:
SELECT e.salario
FROM Empleado e
WHERE e.departamento.nombre ='Desarrollo'AND e.nombre ='CALQUERA'' OR
''Pepe'' = ''Pepe'
Las comillas simples se escapan añadiéndoles una comilla simple adicional. Esto elimina cualquier significado especial de ellas y toda la secuencia se trata como un único valor de cadena.
Recomendación
Se recomienda el uso de consultas con nombre definidas estáticamente, especialmente para
consultas que se ejecutan con frecuencia (siguiente sección). Si las consultas dinámicas son necesarias debe usarse la vinculación de parámetros en lugar de concatenar valores de parámetros en cadenas de consulta para minimizar la cantidad de cadenas de consulta distintas analizadas por el motor de consultas.
3.2. Consultas con nombre
Las consultas nombradas permiten organizar las definiciones de consultas y mejorar el rendimiento de la aplicación.
Una consulta nombrada se define utilizando la anotación @NamedQuery, que puede colocarse en la definición de clase para cualquier entidad. La anotación define el nombre de la consulta, así como el texto de la consulta:
Declaración de una consulta nombrada:
@NamedQuery(name="encontrarSalarioPorNombreYDepartamento",
query="SELECT e.salario FROM Empleado e "+"WHERE e.departamento.nombre = :nombreDepartamento AND "+" e.nombre = :nombreEmpleado")
Las consultas nombradas se colocan normalmente en la clase de entidad que más corresponde directamente al resultado de la consulta, por lo que la entidad Empleado sería un buen lugar para esta consulta nombrada.
La anotación @NamedQuerypuede aparecer varias veces en una clase de entidad. Esto es útil si la entidad tiene varias consultas que se utilizan con frecuencia. Las consultas nombradas se pueden referenciar por nombre en cualquier lugar donde se pueda usar una consulta dinámica. Tienen dos elementos obligatorios:
name: String con el nombre de la consulta.
query: String con el texto de la consulta.
Uso de una consulta nombrada:
publicclassServicioConsulta {
@PersistenceContext(unitName="UnidadConsultas") // Se usa cuando se inyecta el EntityManager de la unidad de persistencia "UnidadConsultas" y es gestionado por el contenedor (no para aplicaciones Java SE). EntityManager em;
publiclongconsultarSalarioEmpleado(String nombreDepartamento, String nombreEmpleado) {
return em.createNamedQuery("encontrarSalarioPorNombreYDepartamento", Long.class)
.setParameter("nombreDepartamento", nombreDepartamento)
.setParameter("nombreEmpleado", nombreEmpleado)
.getSingleResult();
}
}
La API de Persistencia de Jakarta proporciona dos métodos para consultar entidades:
El lenguaje de consulta de la API de Persistencia de Jakarta (Jakarta Persistence QL).
API Criteria.
1.1. Jakarta Persistence QL
Jakarta Persistence QL es el lenguaje de consulta estándar basado en String de la API de Persistencia de Jakarta.
Es un lenguaje de consulta portable que combina la sintaxis y la semántica de SQL con la de un lenguaje de expresión orientado a objetos. Las consultas escritas utilizando este lenguaje pueden compilarse de forma portátil a SQL en todos los servidores de bases de datos principales.
1.2. API de Criterios
La API Criteria se utiliza para crear consultas seguras para el tipo utilizando las API del lenguaje de programación Java al consultar entidades y sus relaciones.
De momento, no estudiaremos el API de Criteria, y nos centraremos en JPQL.
2. Introducción a Jakarta Persistence QL
Jakarta Persistence QL no es SQL. A pesar de las similitudes entre los dos lenguajes en cuanto a palabras clave y estructura general, existen diferencias muy importantes. Intentar escribir Jakarta Persistence QL como si fuera SQL es la forma más fácil de frustrarse con el lenguaje. Las similitudes entre ambos lenguajes son intencionales (brindando a los desarrolladores una idea de lo que Jakarta Persistence QL puede lograr), pero la naturaleza orientada a objetos de Jakarta Persistence QL requiere un tipo diferente de pensamiento.
Naturaleza de Jakarta Persistence QL
Si Jakarta Persistence QL no es SQL, Jakarta Persistence QL es un lenguaje para consultar entidades.
En lugar de tablas y filas, proporciona consultas en términos de entidades y sus relaciones, operando sobre el estado persistente de la entidad definido en el modelo de objetos, no en el modelo físico de la base de datos.
A diferencia de SQL, es portable. Jakarta Persistence QL se puede traducir a los dialectos SQL de todos los principales proveedores de bases de datos.
Las consultas se escriben contra el modelo de dominio de entidades persistentes, sin necesidad de saber exactamente cómo se asignan esas entidades a la base de datos.
APIs de Jakarta Persistence QL vs Criteria:
Las consultas de Jakarta Persistence QL son generalmente más concisas y legibles que las consultas de Criteria. Jakarta Persistence QL es fácil de aprender para programadores con conocimientos previos de SQL.
Las consultas de Jakarta Persistence QL no son seguras en cuanto a tipos, lo que significa que requieren una conversión cuando se recupera el resultado de la consulta del administrador de entidades. Debido a eso, los errores de conversión de tipo pueden no detectarse en tiempo de compilación. Además, las consultas de Jakarta Persistence QL no admiten parámetros de final abierto.
Las consultas de Criteria API son seguras en cuanto a tipos y, por lo tanto, no requieren conversión.
En cuanto a rendimiento entre Jakarta Persistence QL y Criteria APIs, las consultas de Criteria API proporcionan un mejor rendimiento porque las consultas dinámicas de Jakarta Persistence QL deben analizarse cada vez que se llaman.
Una de las desventajas comunes de las consultas de Criteria API es que suelen requerir escribir más código que las consultas de Jakarta Persistence QL. Esto significa que los programadores deberán crear muchos objetos y realizar operaciones en esos objetos antes de enviar la consulta de Criteria API al administrador de entidades.
Una amplia selección de características de SQL son compatibles directamente, incluidas subconsultas, consultas de agregados, declaraciones UPDATE y DELETE, numerosas funciones de SQL, y más.
2.1. Terminología
Las consultas se dividen en una de cuatro categorías: select, aggregate, update y delete.
Las consultas select recuperan el estado persistente de una o más entidades, filtrando los resultados según sea necesario
Las consultas de agregación son variaciones de las consultas select que agrupan los resultados y producen datos resumidos. Juntas, las consultas select y de agregado a veces se llaman consultas de informes, ya que se centran principalmente en generar datos para informes.
Las consultas de actualización y eliminación se utilizan para modificar o eliminar condicionalmente conjuntos enteros de entidades.
Las consultas operan en el conjunto de entidades y embebidos definidos por una unidad de persistencia. Este conjunto de entidades y embebidos se conoce como el esquema de persistencia abstracto, cuya colección define el dominio general desde el cual se pueden recuperar los resultados.
En las expresiones de consulta, las entidades se refieren por nombre. Si una entidad no ha sido nombrada explícitamente (por ejemplo, utilizando el atributo de nombre de la anotación @Entity), se utiliza el nombre de clase no calificado de forma predeterminada. Este nombre es el nombre de esquema abstracto de la entidad en el contexto de una consulta.
Las entidades están compuestas por una o más propiedades de persistencia implementadas como campos o propiedades JavaBean. El tipo de esquema abstracto de una propiedad persistente en una entidad se refiere a la clase o tipo primitivo utilizado para implementar esa propiedad. Por ejemplo, si la entidad Empleado tiene una propiedad nombre de tipo String, el tipo de esquema abstracto de esa propiedad en las expresiones de consulta también es String. Las propiedades persistentes simples sin asignación de relaciones comprenden el estado persistente de la entidad y se denominan campos de estado. Las propiedades persistentes que también son relaciones se llaman campos de asociación.
Las consultas se pueden definir dinámicamente o estáticamente. Los ejemplos incluyen consultas que pueden usarse dinámicamente o estáticamente, según las necesidades de la aplicación.
Finalmente, es importante destacar que las consultas no distinguen entre mayúsculas y minúsculas excepto en dos casos: los nombres de entidades y propiedades deben especificarse exactamente como se nombran.
3. Consultas SELECT
Las consultas SELECT son el tipo de consulta más significativo y facilitan la recuperación masiva de datos de la base de datos. No sorprendentemente, las consultas SELECT también son la forma más común de consulta utilizada en las aplicaciones. La forma general de una consulta SELECT es la siguiente:
La forma más simple de una consulta SELECT consta de dos partes obligatorias: la cláusula SELECT y la cláusula FROM. La cláusula SELECT define el formato de los resultados de la consulta, mientras que la cláusula FROM define la entidad o entidades de las cuales se obtendrán los resultados. Considere la siguiente consulta completa que recupera todos los empleados en la empresa:
SELECT e
FROM Empleado e
La estructura de esta consulta es muy similar a una consulta SQL, pero con un par de diferencias importantes. La primera diferencia es que el dominio de la consulta definido en la cláusula FROM no es una tabla, sino una entidad, en este caso, la entidad Empleado. Como en SQL, se le ha asignado un alias al identificador e. Este valor alias se conoce como una variable de identificación y es la clave por la cual la entidad se referirá en el resto de la declaración SELECT. A diferencia de las consultas en SQL, donde un alias de tabla es opcional, el uso de variables de identificación es obligatorio en Jakarta Persistence QL.
La segunda diferencia es que la cláusula SELECT en este ejemplo no enumera los campos de la tabla ni utiliza un comodín para seleccionar todos los campos. En cambio, solo se enumera la variable de identificación para indicar que el tipo de resultado de la consulta es la entidad Empleado, no un conjunto tabular de filas.
Procesamiento de Consultas y Resultados
A medida que el procesador de consultas itera sobre el conjunto de resultados devuelto por la base de datos, convierte los datos de filas y columnas tabulares en un conjunto de instancias de entidad. El método getResultList() de la interfaz Query devolverá una colección de cero o más objetos Empleado después de evaluar la consulta. A pesar de las diferencias en estructura y sintaxis, cada consulta es traducible a SQL.
Para ejecutar una consulta, el motor de consultas primero construye una representación SQL óptima de la consulta de Jakarta Persistence QL. La consulta SQL resultante es la que realmente se ejecuta en la base de datos. En este ejemplo simple, el SQL podría parecer algo así, dependiendo de los metadatos de mapeo para la entidad Empleado:
SELECT id, nombre, salario, idGestor, idDepartamento, idDireccion
FROM emp
La instrucción SQL debe leer todas las columnas mapeadas necesarias para crear la instancia de entidad, incluidas las columnas de clave externa. Incluso si la entidad está en caché en la memoria, el motor de consultas aún leerá típicamente todos los datos necesarios para asegurarse de que la versión en caché esté actualizada. Tenga en cuenta que si las relaciones entre Empleado y las entidades Departamento o Direccion requirieran una carga ansiosa (eager loading), la instrucción SQL se extendería para recuperar los datos adicionales o se agruparían múltiples instrucciones para construir completamente la entidad Empleado. Cada proveedor proporcionará algún método para mostrar el SQL que genera al traducir Jakarta Persistence QL. Para la optimización del rendimiento en particular, comprender cómo se aborda la generación de SQL por parte de su proveedor puede ayudarlo a escribir consultas más eficientes.
Ahora que hemos revisado una consulta simple y cubierto la terminología básica, las siguientes secciones pasarán por cada una de las cláusulas de la consulta SELECT, explicando la sintaxis y las características disponibles.
Nota: la función de transmisión de consultas incluida en Jakarta Persistence nos ayudará a evitar la recuperación de demasiados datos y provocar errores. Sin embargo, aún se recomienda y es más eficiente utilizar el método de paginación de Result Set.
3.1. Cláusula SELECT
La cláusula SELECT de una consulta puede tener varias formas, incluyendo expresiones de ruta simples y complejas, expresiones escalares, expresiones de constructor, funciones agregadas y secuencias de estos tipos de expresiones. Las siguientes secciones introducen las expresiones de ruta y discuten los diferentes estilos de cláusulas SELECT y cómo determinan el tipo de resultado de la consulta. Dejamos la discusión de las expresiones escalares para explorar las expresiones condicionales en la cláusula WHERE. Están completamente descritas en la sección llamada “Expresiones Escalares”. Las funciones agregadas se detallan en el apartado “Consultas Agregadas”.
3.1.1. Expresiones de Ruta
Se utilizan para navegar desde una entidad, ya sea a través de una relación hacia otra entidad (o colección de entidades) o hacia una de las propiedades persistentes de una entidad. La navegación que resulta en uno de los campos de estado persistentes (ya sea campo o propiedad) de una entidad se denomina ruta de campo de estado. La navegación que lleva a una sola entidad se llama ruta de asociación de un solo valor, mientras que la navegación hacia una colección de entidades se llama ruta de asociación de un valor de colección.
El operador de punto (.) significa navegación de ruta en una expresión. Por ejemplo, si la entidad Empleado se ha asignado a la variable de identificación e, e.nombre es una expresión de ruta de campo de estado que resuelve al nombre del empleado. De manera similar, la expresión de ruta e.departamento es una asociación de un solo valor desde el empleado hacia el departamento al que está asignado. Finalmente, e.directos es una asociación de un valor de colección que se resuelve en la colección de empleados que reportan a un empleado que también es un gerente.
Lo que hace que las expresiones de ruta sean tan poderosas es que no están limitadas a una sola navegación. En su lugar, las expresiones de navegación se pueden encadenar para recorrer grafos de entidades complejos siempre que la ruta se mueva de izquierda a derecha a través de asociaciones de un solo valor. Una ruta no puede continuar desde un campo de estado o una asociación de un valor de colección.
Usando esta técnica, podemos construir expresiones de ruta como e.departamento.nombre, que es el nombre del departamento al que pertenece el empleado. Tenga en cuenta que las expresiones de ruta pueden navegar hacia objetos incrustados y a través de ellos, así como a entidades normales. La única restricción en los objetos incrustados en una expresión de ruta es que la raíz de la expresión de ruta debe comenzar con una entidad.
Las expresiones de ruta se utilizan en cada cláusula de una consulta SELECT, determinando desde el tipo de resultado de la consulta hasta las condiciones bajo las cuales se deben filtrar los resultados. La experiencia con las expresiones de ruta es clave para escribir consultas efectivas.
3.1.2. Entidades y Objetos
La primera y más simple forma de la cláusula SELECT es una única variable de identificación. El tipo de resultado para una consulta de este estilo es la entidad a la que está asociada la variable de identificación. Por ejemplo, la siguiente consulta devuelve todos los departamentos en la empresa:
SELECT d
FROM Departamento d
La palabra clave OBJECT se puede utilizar para indicar que el tipo de resultado de la consulta es la entidad vinculada a la variable de identificación. No tiene impacto en la consulta, pero se puede usar como una pista visual:
SELECTOBJECT(d)
FROM Departamento d
El único problema de usar OBJECT es que, aunque las expresiones de ruta pueden resolverse a un tipo de entidad, la sintaxis de la palabra clave OBJECT está limitada a variables de identificación. La expresión OBJECT(e.departamento) es ilegal incluso si Departamento es un tipo de entidad. Por esa razón, no se recomienda la sintaxis de OBJECT. Existe principalmente por compatibilidad con versiones anteriores del lenguaje que requerían la palabra clave OBJECT bajo el supuesto de que una revisión futura de SQL incluiría la misma terminología.
Una expresión de ruta que resuelve a un campo de estado o asociación de un solo valor también se puede utilizar en la cláusula SELECT. En este caso, el tipo de resultado de la consulta se convierte en el tipo de la expresión de ruta, ya sea el tipo de campo de estado o el tipo de entidad de una asociación de un solo valor. La siguiente consulta devuelve los nombres de todos los empleados:
SELECT e.nombre
FROM Empleado e
El tipo de resultado de la expresión de ruta en la cláusula SELECT es String, por lo que ejecutar esta consulta con getResultList() producirá una colección de cero o más objetos String.
Las expresiones de ruta que resuelven en campos de estado también se pueden usar como parte de expresiones escalares, lo que permite transformar el campo de estado en los resultados de la consulta. Discutiremos esta técnica más adelante en la sección llamada “Expresiones Escalares”.
Las entidades alcanzadas desde una expresión de ruta también se pueden devolver. La siguiente consulta demuestra devolver una entidad diferente como resultado de la navegación de la ruta:
SELECT e.departamento
FROM Empleado e
El tipo de resultado de esta consulta es la entidad Departamento porque es el resultado de atravesar la relación del departamento desde Empleado hasta Departamento. Ejecutar la consulta, por lo tanto, dará como resultado una colección de cero o más objetos Departamento, incluidos duplicados.
Para eliminar los duplicados, se debe utilizar el operador DISTINCT:
SELECTDISTINCT e.departamento
FROM Empleado e
El operador DISTINCT es funcionalmente equivalente al operador del mismo nombre en SQL. Una vez que se recopila el conjunto de resultados, se eliminan los valores duplicados (usando la identidad de la entidad si el tipo de resultado de la consulta es una entidad), de modo que solo se devuelven resultados únicos.
El tipo de resultado de una consulta SELECT es el tipo correspondiente a cada fila en el conjunto de resultados producido al ejecutar la consulta. Esto puede incluir entidades, tipos primitivos y otros tipos de atributos persistentes, pero nunca un tipo de colección. La siguiente consulta es ilegal:
SELECT d.empleados
FROM Departamento d
La expresión de ruta d.empleados es una ruta de valor de colección que produce un tipo de colección. Restringir las consultas de esta manera evita que el proveedor tenga que combinar filas sucesivas de la base de datos en un solo objeto de resultado.
Es posible seleccionar objetos incrustables navegados en una expresión de ruta. La siguiente consulta devuelve solo los objetos incrustables InformacionContacto para todos los empleados:
SELECT e.informacionContacto
FROM Empleado e
Lo importante de recordar al seleccionar objetos incrustables es que los objetos devueltos no estarán gestionados. Si emites una consulta para devolver empleados (SELECT e FROM Empleado e) y luego, desde los resultados, navegas a sus objetos incrustados InformacionContacto, estarías obteniendo objetos incrustables que estaban gestionados. Los cambios en cualquiera de esos objetos se guardarían cuando se confirmara la transacción. Sin embargo, cambiar cualquiera de los resultados de objetos InformacionContacto devueltos de una consulta que seleccionó directamente InformacionContacto, no tendría ningún efecto persistente.
3.1.3 Combinación de Expresiones
Se pueden especificar múltiples expresiones en la misma cláusula SELECT separándolas con comas. El tipo de resultado de la consulta en este caso es un array de tipo Object, donde los elementos del array son los resultados de resolver las expresiones en el orden en que aparecieron en la consulta.
Considera la siguiente consulta que devuelve solo el nombre y salario de un empleado:
SELECT e.nombre, e.salario
FROM Empleado e
Cuando se ejecuta esto, se devolverá una colección de cero o más instancias de arrays de tipo Object. Cada array en este ejemplo tiene dos elementos, el primero es un String que contiene el nombre del empleado y el segundo es un Double que contiene el salario del empleado. La práctica de informar solo un subconjunto de los campos de estado de una entidad se llama proyección porque los datos de la entidad se proyectan desde la entidad en forma tabular. La proyección es una técnica útil para aplicaciones web en las que solo se muestran unos pocos datos de un gran conjunto de instancias de entidades. Dependiendo de cómo se haya mapeado la entidad, podría requerir una consulta SQL compleja para recuperar completamente el estado de la entidad. Si solo se requieren dos campos, el esfuerzo adicional invertido en la construcción de la instancia de entidad podría haber sido desperdiciado. Una consulta de proyección que devuelve solo la cantidad mínima de datos es más útil en estos casos.
3.1.4 Constructor de expresiones
Una forma más potente de la cláusula SELECT que implica múltiples expresiones es la expresión de construcción (NEW), que especifica que los resultados de la consulta se deben almacenar utilizando un tipo de objeto especificado por el usuario. Considera la siguiente consulta:
SELECTNEW ejemplo.DetalleEmpleado(e.nombre, e.salario, e.departamento.nombre)
FROM Empleado e
El tipo de resultado de esta consulta es la clase Java ejemplo.DetalleEmpleado. A medida que el procesador de consultas itera sobre los resultados de la consulta, se crean instancias de DetalleEmpleado utilizando el constructor que coincide con los tipos de expresión enumerados en la consulta. En este caso, los tipos de expresión son String, Double y String. Cada fila en la colección de consulta resultante es una instancia de DetalleEmpleado que contiene el nombre del empleado, el salario y el nombre del departamento.
El tipo de objeto de resultado debe referirse utilizando el nombre completo del objeto. Sin embargo, la clase no tiene que estar mapeada en la base de datos de ninguna manera. Cualquier clase con un constructor compatible con las expresiones enumeradas en la cláusula SELECT se puede usar en una expresión de constructor.
Las expresiones de constructor son herramientas poderosas para construir objetos de transferencia de datos o de vista de grano grueso para su uso en otros niveles de la aplicación. En lugar de construir manualmente estos objetos, se puede usar una sola consulta para reunir objetos de vista listos para su presentación en una página web.
3.2. Cláusula FROM
La cláusula FROM se utiliza para declarar una o más variables de identificación, opcionalmente derivadas de relaciones unidas, que forman el dominio sobre el cual la consulta debería extraer sus resultados. La sintaxis de la cláusula FROM consiste en una o más variables de identificación y declaraciones de cláusulas JOIN.
3.2.1 Variables de Identificación
La variable de identificación es el punto de partida para todas las expresiones de consulta. Cada consulta debe tener al menos una variable de identificación definida en la cláusula FROM, y esa variable debe corresponder a un tipo de entidad. Cuando una declaración de variable de identificación no utiliza una expresión de ruta (es decir, cuando es un solo nombre de entidad), se denomina declaración de variable de rango. Esta terminología proviene de la teoría de conjuntos, ya que se dice que la variable abarca la entidad.
Las declaraciones de variables de rango utilizan la sintaxis <nombre_entidad> [AS] <identificador>.
La palabra clave AS opcional. El identificador debe seguir las reglas de nomenclatura estándar de Java y se puede referenciar en toda la consulta de manera insensible a mayúsculas y minúsculas. Se pueden especificar múltiples declaraciones separándolas con comas.
Las expresiones de ruta también pueden tener un alias con las variables de identificación en el caso de uniones y subconsultas. La sintaxis para las declaraciones de variables de identificación en estos casos se cubrirá en las dos secciones siguientes.
3.2.2. Joins
Una operación de unión (join) es una consulta que combina resultados de varias entidades. Los Joins en Jakarta Persistence QL son lógicamente equivalentes a los de SQL. En última instancia, una vez que la consulta se traduce a SQL, es muy probable que las uniones entre entidades produzcan uniones similares entre las tablas a las que están mapeadas las entidades. Comprender cuándo ocurren las uniones es importante para escribir consultas eficientes.
Las uniones ocurren siempre que se cumplen cualquiera de las siguientes condiciones en una consulta SELECT:
Se enumeran dos o más declaraciones de variables de rango en la cláusula FROM y aparecen en la cláusula SELECT.
Se utiliza el operador JOIN para extender una variable de identificación mediante una expresión de ruta.
Una expresión de ruta en cualquier lugar de la consulta navega a través de un campo de asociación, ya sea a la misma entidad o a una entidad diferente.
Una o más condiciones WHERE comparan atributos de diferentes variables de identificación.
La semántica de una unión entre entidades es la misma que las uniones SQL entre tablas. La mayoría de las consultas contienen una serie de condiciones de unión, que son expresiones que definen las reglas para emparejar una entidad con otra.
Las condiciones de unión se pueden especificar explícitamente, como el uso del operador JOIN en la cláusula FROM de una consulta, o implícitamente como resultado de la navegación de la ruta.
Una Inner Join entre dos entidades devuelve los objetos de ambos tipos de entidad que satisfacen todas las condiciones de unión. La navegación de la ruta de una entidad a otra es una forma de unión interna.
La Outer Join de dos entidades es el conjunto de objetos de ambos tipos de entidad que satisfacen las condiciones de unión, más el conjunto de objetos de un tipo de entidad (designado como la entidad izquierda) que no tienen una condición de unión coincidente en el otro.
En ausencia de condiciones de unión entre dos entidades, las consultas producirán un producto cartesiano. Cada objeto del primer tipo de entidad se emparejará con cada objeto del segundo tipo de entidad, multiplicando el número de resultados. Los productos cartesianos son raros en las consultas de Jakarta Persistence QL dadas las capacidades de navegación del lenguaje, pero son posibles si se especifican dos declaraciones de variables de rango en la cláusula FROM sin condiciones adicionales especificadas en la cláusula WHERE.
A. Inner Join
Como lenguaje relacional, Jakarta Persistence QL admite consultas que se basan en múltiples entidades y las relaciones entre ellas.
Las uniones internas entre dos entidades se pueden especificar de dos formas:
La primera y preferida forma, porque es explícita y obvia que se está produciendo una unión, es el operador JOIN en la cláusula FROM.
Otra forma requiere múltiples declaraciones de variables de rango en la cláusula FROM y condiciones de la cláusula WHERE para proporcionar las condiciones de la unión.
A.1. Operador JOIN y campos de asociación de colección (one-to-many y many-to-many)
La sintaxis de una unión interna mediante el operador JOIN es [INNER] JOIN <expresión_de_ruta> [AS] <identificador>. Considera la siguiente consulta:
SELECT p
FROM Empleado e JOIN e.telefonos p
Esta consulta utiliza el operador JOIN para unir la entidad Empleado con la entidad Telefono a través de la relación telefonos. La condición de unión en esta consulta está definida por el mapeo objeto-relacional de la relación telefonos. No es necesario especificar criterios adicionales para vincular las dos entidades. Al unir las dos entidades, esta consulta devuelve todas las instancias de la entidad Telefono asociadas a los empleados de la empresa.
La sintaxis para las uniones es similar a las expresiones JOIN admitidas por ANSI SQL. Otra forma equivalente en SQL escrita utilizando la forma de join tradicional:
SELECT p.id, p.numeroTelefono, p.tipo, p.idEmpleado
FROM emp e, telefono p
WHERE e.id = p.idEmpleado
El mapeo de la tabla para la entidad Telefono reemplaza la expresión e.telefonos. La cláusula WHERE también incluye los criterios necesarios para unir las dos tablas a través de las columnas de unión definidas por el mapeo de telefonos.
Ten en cuenta que la relación telefonos se ha asignado a la variable de identificación p. Aunque la entidad Telefono no aparece directamente en la consulta, el objetivo de la relación telefonos es la entidad Telefono, y esto determina el tipo de la variable de identificación. Esta determinación implícita del tipo de variable de identificación puede llevar algo de tiempo acostumbrarse. Es necesario estar familiarizado con cómo se definen las relaciones en el modelo de objetos para navegar a través de una consulta escrita.
Cada ocurrencia de p fuera de la cláusula FROM ahora se refiere a un solo teléfono propiedad de un empleado. Aunque se especificó un campo de asociación de colección en la cláusula JOIN, la variable de identificación realmente se refiere a entidades alcanzadas por esa asociación, no a la colección en sí. Ahora se puede usar la variable como si la entidad Telefono estuviera listada directamente en la cláusula FROM. Por ejemplo, en lugar de devolver instancias de la entidad Telefono, se pueden devolver solo los números de teléfono:
SELECT p.numero
FROM Empleado e JOIN e.telefonos p
En la definición de expresiones de ruta anterior, se señaló que una ruta no podía continuar desde un campo de estado o un campo de asociación de colección. Para resolver esta situación, el campo de asociación de colección debe unirse en la cláusula FROM para que se cree una nueva variable de identificación para la ruta, lo que permite que sea la raíz de nuevas expresiones de ruta.
IN VS. JOIN
EJB QL, según lo definido por las especificaciones de EJB, utilizaba un operador especial llamado IN en la cláusula FROM para mapear asociaciones de colecciones a variables de identificación. El soporte para este operador se trasladó a Jakarta Persistence QL. La forma equivalente de la consulta utilizada anteriormente en esta sección podría especificarse como:
SELECTDISTINCT p
FROM Empleado e, IN(e.telefonos) p
El operador IN tiene la intención de indicar que la variable p es una enumeración de la colección telefonos. Sin embargo, el operador JOIN es una forma más potente y expresiva de declarar relaciones y es el operador recomendado para consultas.
A.2. Operador JOIN y campos de asociación de un solo valor (one-to-one y many-to-one)
El operador JOIN funciona tanto con expresiones de ruta de asociación de valor de colección como con expresiones de ruta de asociación de valor único. Considera el siguiente ejemplo:
SELECT d
FROM Empleado e JOIN e.departamento d
Esta consulta define un join desde Empleado hasta Departamento a través de la relación departamento. Esto es semánticamente equivalente a usar una expresión de ruta en la cláusula SELECT para obtener el departamento del empleado. Por ejemplo, la siguiente consulta debería resultar en representaciones SQL similares, si no idénticas, que involucren un join entre las entidades Empleado y Departamento:
SELECT e.departamento
FROM Empleado e
El caso de uso principal para usar una expresión de ruta de asociación de valor único en la cláusula FROM (en lugar de solo usar una expresión de ruta en la cláusula SELECT) es para joins externos. La navegación de la ruta es equivalente al inner join de todas las entidades asociadas atravesadas en la expresión de ruta.
La posibilidad de joins internos implícitos resultantes de expresiones de ruta es algo a tener en cuenta. Considera el siguiente ejemplo que devuelve los departamentos distintos basados en California que participan en el proyecto Versión1:
SELECTDISTINCT e.departamento
FROM Proyecto p JOIN p.empleados e
WHERE p.nombre ='Versión1'ANDe.direccion.provincia ='PO'
En realidad, hay cuatro joins lógicos aquí, no dos. El traductor tratará la consulta como si se hubiera escrito con joins explícitos entre las diversas entidades. Cubriremos la sintaxis para múltiples joins más adelante en la sección “Múltiples Joins”, pero por ahora considera la siguiente consulta que es equivalente a la consulta anterior, leyendo las condiciones de join de izquierda a derecha:
SELECTDISTINCT d
FROM Proyecto p JOIN p.empleados e JOIN e.departamento d JOIN e.direccion a
WHERE p.nombre ='Versión1'ANDa.provincia ='PO'
Decimos cuatro joins lógicos porque el mapeo físico real podría involucrar más tablas. En este caso, las entidades Empleado y Proyecto están relacionadas a través de una asociación many-to-many que utiliza una tabla de join. Por lo tanto, el SQL real para tal consulta usa cinco tablas, no cuatro. La consulta se vería así:
SELECTDISTINCT d.id, d.nombre
FROM project p, proyectosEmpleado ep, emp e, dept d, direccion a
WHERE p.id = ep.idProyecto ANDep.idEmpleado = e.id ANDe.idDepartamento = d.id ANDe.idDireccion = a.id ANDp.nombre ='Versión1'ANDa.provincia ='PO'
La primera forma de la consulta ciertamente es más fácil de leer y entender. Sin embargo, durante la optimización del rendimiento, podría ser útil comprender cuántos joins pueden ocurrir como resultado de expresiones de ruta aparentemente triviales.
A.3. Condiciones de Join en la Cláusula WHERE
Las consultas SQL tradicionalmente han unido tablas enumerando las tablas a unir en la cláusula FROM y proporcionando criterios en la cláusula WHERE de la consulta para determinar las condiciones de join. Para unir dos entidades sin usar una relación, se utiliza una declaración de variable de rango para cada entidad en la cláusula FROM.
El ejemplo de join anterior entre las entidades Empleado y Departamento también podría haberse escrito de la siguiente manera:
SELECTDISTINCT d
FROM Departamento d, Empleado e
WHERE d = e.departamento
Este estilo de consulta se utiliza generalmente para compensar la falta de una relación explícita entre dos entidades en el modelo de dominio. Por ejemplo, no hay una asociación entre la entidad Departamento y el Empleado que es el gerente del departamento.
Podemos usar una condición de join en la cláusula WHERE para hacer esto posible:
SELECT d, m
FROM Departamento d, Empleado m
WHERE d = m.departamento ANDm.directos ISNOT EMPTY
En este ejemplo, estamos utilizando una de las expresiones de colección especiales, IS NOT EMPTY, para verificar que la colección de informes directos al empleado no está vacía. Cualquier empleado con una colección no vacía de informes directos es, por definición, un gerente.
A.4. Múltiples Joins
Más de un join se puede concatenar si es necesario. Por ejemplo, la siguiente consulta devuelve el conjunto distinto de proyectos pertenecientes a empleados que pertenecen a un departamento:
SELECTDISTINCT p
FROM Departamento d JOIN d.empleados e JOIN e.proyectos p
El procesador de consultas interpreta la cláusula FROM de izquierda a derecha. Una vez que se ha declarado una variable, puede ser referenciada posteriormente por otras expresiones JOIN. En este caso, la relación proyectos de la entidad Empleado se navega una vez que se ha declarado la variable empleado.
A.5. Joins con Mapas: VALUE, KEY y ENTRY
Una expresión de ruta que navega a través de una asociación de valor de colección implementada como un Map es un caso especial. A diferencia de una colección normal, cada elemento en un mapa corresponde a dos piezas de información: la clave y el valor.
Al trabajar con Jakarta Persistence QL, es importante tener en cuenta que, por defecto, las variables de identificación basadas en mapas se refieren al valor. Por ejemplo, considera el caso en el que la relación telefonos de la entidad Empleado se modela como un mapa, donde la clave es el tipo de número (trabajo, celular, casa, etc.) y el valor es el número de teléfono. La siguiente consulta enumera los números de teléfono de todos los empleados:
SELECT e.nombre, p
FROM Empleado e JOIN e.telefonos p
Este comportamiento se puede resaltar explícitamente mediante el uso de la palabra clave VALUE. Por ejemplo, la consulta anterior es funcionalmente idéntica a la siguiente:
SELECT e.nombre, VALUE(p)
FROM Empleado e JOIN e.telefonos p
Para acceder a la clave en lugar del valor para un elemento de mapa dado, podemos usar la palabra clave KEY para anular el comportamiento predeterminado y devolver el valor de la clave para un elemento de mapa dado:
SELECT e.nombre, KEY(p), VALUE(p)
FROM Empleado e JOIN e.telefonos p
WHEREKEY(p) IN ('Trabajo', 'Móvil')
Finalmente, en caso de que queramos que tanto la clave como el valor se devuelvan juntos en forma de un objeto java.util.Map.Entry, podemos especificar la palabra clave ENTRY de la misma manera. Ten en cuenta que la palabra clave ENTRY solo se puede usar en la cláusula SELECT. Las palabras clave KEY y VALUE también se pueden usar como parte de expresiones condicionales en las cláusulas WHERE y HAVING de la consulta.
Es importante señalar que en cada uno de los ejemplos de unión de mapas, unimos una entidad contra uno de sus atributos de Mapa y obtenemos una clave, un valor o un par clave-valor (entrada). Sin embargo, cuando se ve desde la perspectiva de las tablas, la unión se realiza solo a nivel de la clave principal de la entidad de origen y los valores en el Mapa. Actualmente, no hay una facilidad disponible en Jakarta Persistence para unir la entidad de origen contra las claves del Mapa.
B. Outer Joins
Un outer join entre dos entidades produce un dominio en el cual solo se requiere que un lado de la relación esté completo.
En otras palabras, el join externo de Empleado a Departamento a través de la relación de departamento de empleado devuelve todos los empleados y el departamento al cual se le ha asignado el empleado, pero el departamento se devuelve solo si está disponible. Esto contrasta con un inner join que devolvería solo aquellos empleados asignados a un departamento.
Un join externo se especifica utilizando la siguiente sintaxis: LEFT [OUTER] JOIN <path_expression> [AS] <identifier>. La siguiente consulta demuestra un join externo entre dos entidades:
SELECT e, d
FROM Empleado e LEFTJOIN e.departamento d
Si el empleado no ha sido asignado a un departamento, el objeto de departamento (el segundo elemento del array de Object) será nulo.
En una generación SQL típica del proveedor, verás que la consulta anterior sería equivalente a la siguiente:
SELECT e.id, e.nombre, e.salario, e.idGestor, e.idDepartamento, e.idDireccion,
d.id, d.nombre
FROM empleado e LEFTOUTERJOIN departamento d
ON (d.id = e.idDepartamento)
El SQL resultante muestra que cuando se genera un join externo desde Jakarta Persistence QL, siempre especifica una condición ON de igualdad entre la columna de join que mapea la relación que se está uniendo y la clave primaria a la que se está haciendo referencia.
Se puede suministrar una expresión ON adicional para agregar restricciones a los objetos que se devuelven desde el lado derecho del join. Por ejemplo, podemos modificar la consulta Jakarta Persistence QL anterior para tener una condición ON adicional que limite los departamentos devueltos solo a aquellos que tienen un prefijo ‘QA’:
SELECT e, d
FROM Empleado e LEFTJOIN e.departamento d
ON d.nombre LIKE'De%'
Esta consulta sigue devolviendo todos los empleados, pero los resultados no incluirán ningún departamento que no coincida con la condición ON agregada. El SQL generado se vería así:
SELECT e.id, e.nombre, e.salario, e.idDepartamento, e.idGestor, e.idDireccion,
d.id, d.nombre
FROM empleado e leftouterjoin departamento d
ON ((d.id = e.idDepartamento) and (d.nombre like'De%'))
Es importante señalar que esta consulta es muy diferente de usar una expresión WHERE:
SELECT e, d
FROM Empleado e LEFTJOIN e.departamento d
WHERE d.nombre LIKE'De%'
La expresión WHERE limitará los resultados después de realizar el join, lo que puede resultar en un comportamiento diferente:
SELECT e, d
FROM Empleado e LEFTJOIN e.departamento d
WHERE d.nombre LIKE'De%'
La cláusula WHERE resulta en una semántica de inner join entre Empleado y Departamento, por lo que esta consulta solo devolvería los empleados que estuvieran en un departamento con un nombre que comience con ‘QA’.
C. Fetch Joins
Los fetch joins están destinados a ayudar a los diseñadores de aplicaciones a optimizar el acceso a su base de datos y preparar los resultados de la consulta para su desprendimiento. Permiten que las consultas especifiquen una o más relaciones que deben ser navegadas y pre-cargadas por el motor de consulta para que no se carguen de forma diferida más tarde en tiempo de ejecución.
Por ejemplo, si tenemos una entidad Empleado con una relación de carga diferida con su dirección, la siguiente consulta se puede utilizar para indicar que la relación debe resolverse de forma inmediata durante la ejecución de la consulta:
SELECT e
FROM Empleado e JOINFETCH e.direccion
No se establece una variable de identificación para la expresión de ruta e.direccion. Esto se debe a que aunque la entidad Direccion se está uniendo para resolver la relación, no es parte del tipo de resultado de la consulta. El resultado de ejecutar la consulta sigue siendo una colección de instancias de la entidad Empleado, excepto que la relación de dirección en cada entidad no causará una consulta secundaria a la base de datos cuando se acceda a ella. Esto también permite que la relación de dirección se acceda de forma segura si la entidad Empleado se vuelve desprendida.
Un fetch join se distingue de un join regular al agregar la palabra FETCH al operador JOIN.
Para implementar fetch joins, el proveedor necesita convertir la asociación recuperada en un join regular del tipo apropiado: interno por defecto o externo si se especificó la palabra LEFT. La expresión SELECT de la consulta también necesita expandirse para incluir la relación unida. Expresado en Jakarta Persistence QL, una interpretación equivalente del proveedor del ejemplo anterior de fetch join se vería así:
SELECT e, a
FROM Empleado e JOIN e.direccion a
La única diferencia es que el proveedor en realidad no devuelve las entidades Direccion al llamante. Debido a que los resultados se procesan desde esta consulta, el motor de consultas crea la entidad Direccion en la memoria y la asigna a la entidad Empleado, pero luego la elimina de la colección de resultados que construye para el cliente. Esto carga de forma anticipada la relación de direcciones, que luego se puede acceder mediante la navegación desde la entidad Empleado.
Una consecuencia de implementar fetch joins de esta manera es que la obtención de una colección asociativa produce resultados duplicados. Por ejemplo, considere una consulta de departamento donde la relación empleados de la entidad Departamento se obtiene de manera anticipada. La consulta de fetch join, esta vez utilizando un outer join para asegurar que se recuperen los departamentos sin empleados, se escribiría de la siguiente manera:
SELECT d
FROM Departamento d LEFTJOINFETCH d.empleados
Expresado en Jakarta Persistence QL, la interpretación del proveedor reemplazaría el fetch con un outer join a través de la relación empleados:
SELECT d, e
FROM Departamento d LEFTJOIN d.empleados e
Nuevamente, a medida que se procesan los resultados, la entidad Empleado se construye en la memoria pero se elimina de la colección de resultados. Cada entidad Departamento ahora tiene una colección de empleados completamente resuelta, pero el cliente recibe una referencia a cada departamento por empleado. Por ejemplo, si se recuperaron cuatro departamentos con cinco empleados cada uno, el resultado sería una colección de 20 instancias de Departamento, con cada departamento duplicado cinco veces. Las instancias reales de las entidades apuntan todas a las mismas versiones administradas, pero los resultados son algo extraños, como mínimo.
Para eliminar los valores duplicados, se debe usar el operador DISTINCT o los resultados deben colocarse en una estructura de datos como un Set. Dado que no es posible escribir una consulta SQL que use el operador DISTINCT y al mismo tiempo preserve la semántica del fetch join, el proveedor deberá eliminar duplicados en la memoria después de que se hayan recuperado los resultados. Esto podría tener implicaciones de rendimiento para conjuntos de resultados grandes.
Dadas los resultados algo peculiares generados por un fetch join a una colección, puede que no sea la forma más apropiada de cargar de forma anticipada entidades relacionadas en todos los casos. Si una colección requiere una carga anticipada de manera regular, considera hacer que la relación sea ansiosa por defecto. Algunos proveedores de persistencia también ofrecen lecturas por lotes como alternativa a los fetch joins, emitiendo múltiples consultas en un solo lote y luego correlacionando los resultados para cargar de forma anticipada las relaciones. Otra alternativa es utilizar un gráfico de entidades para determinar dinámicamente los atributos de relación que se cargarán mediante una consulta.
3.3. Clausula WHERE
La cláusula WHERE de una consulta se utiliza para especificar condiciones de filtrado para reducir el conjunto de resultados. En esta sección, exploramos las características de la cláusula WHERE y los tipos de expresiones que se pueden formar para filtrar los resultados de la consulta.
La definición de la cláusula WHERE es engañosamente simple. Es simplemente la palabra clave WHERE, seguida de una expresión condicional. Sin embargo, como demuestran las siguientes secciones, Jakarta Persistence QL admite un conjunto poderoso de expresiones condicionales para filtrar las consultas más sofisticadas.
Parámetros de entrada
Los parámetros de entrada para las consultas se pueden especificar utilizando notación posicional o con nombre. La notación posicional se define prefijando el número de variable con un signo de interrogación. Considere la siguiente consulta:
SELECT e
FROM Empleado e
WHERE e.salario >?1
Utilizando la interfaz Query, cualquier valor double o un valor compatible con el tipo del atributo salario se puede vincular al primer parámetro para indicar el límite inferior para los salarios de los empleados en esta consulta. El mismo parámetro posicional puede ocurrir más de una vez en la consulta. El valor vinculado al parámetro se sustituirá en cada una de sus ocurrencias.
Los parámetros con nombre se especifican utilizando dos puntos seguidos de un identificador. Aquí está la misma consulta, esta vez usando un parámetro con nombre:
SELECT e
FROM Empleado e
WHERE e.salario > :sal
Forma Básica de Expresión
Gran parte del soporte para expresiones condicionales en Jakarta Persistence QL se toma directamente de SQL. Esto es intencional y ayuda a facilitar la transición para los desarrolladores que ya están familiarizados con SQL. La principal diferencia entre las expresiones condicionales en Jakarta Persistence QL y SQL es que las expresiones en Jakarta Persistence QL pueden aprovechar las variables de identificación y las expresiones de ruta para navegar por las relaciones durante la evaluación de la expresión.
Las expresiones condicionales se construyen de la misma manera que las expresiones condicionales en SQL, utilizando una combinación de operadores lógicos, expresiones de comparación, operaciones primitivas y funciones en campos, entre otros. Aunque se proporciona un resumen de los operadores más adelante, la gramática de las expresiones condicionales no se repite aquí. La especificación de Jakarta Persistence contiene la gramática en la forma de Backus-Naur (BNF) y es el lugar donde buscar las reglas exactas sobre el uso de las expresiones básicas. Sin embargo, las secciones siguientes explican los operadores y expresiones de nivel superior, especialmente aquellos únicos de Jakarta Persistence QL, y proporcionan ejemplos para cada uno.
La sintaxis literal también es similar a SQL (consulte la sección “Literales”).
La precedencia de operadores es la siguiente:
Operador de navegación (.)
Unary +/–
Multiplicación (*) y división (/)
Adición (+) y sustracción (–)
Operadores de comparación =, >, >=, <, <=, <>, [NOT] BETWEEN, [NOT] LIKE, [NOT] IN, IS [NOT] NULL, IS [NOT] EMPTY, [NOT] MEMBER [OF]
Operadores lógicos (AND, OR, NOT)
Expresiones BETWEEN
El operador BETWEEN se puede usar en expresiones condicionales para determinar si el resultado de una expresión cae dentro de un rango inclusivo de valores. Las expresiones numéricas, de cadena y de fecha se pueden evaluar de esta manera. Considere el siguiente ejemplo:
SELECT e
FROM Empleado e
WHERE e.salario BETWEEN40000AND45000
Cualquier empleado que gane $40,000–$45,000 inclusivamente se incluirá en los resultados. Esto es idéntico a la siguiente consulta que utiliza operadores de comparación básicos:
SELECT e
FROM Empleado e
WHERE e.salario >=40000AND e.salario <=45000
El operador BETWEEN también se puede negar con el operador NOT.
Expresiones LIKE
Jakarta Persistence QL admite la condición LIKE de SQL para proporcionar una forma limitada de coincidencia de patrones de cadena. Cada expresión LIKE consta de una expresión de cadena a buscar y una cadena de patrón y una secuencia de escape opcional que define las condiciones de coincidencia. Los caracteres comodín utilizados por la cadena de patrón son el guion bajo (_) para comodines de un solo carácter y el signo de porcentaje (%) para comodines de varios caracteres:
SELECT d
FROM Departamento d
WHERE d.nombre LIKE'__Eng%'
Estamos utilizando un prefijo de dos guiones bajos para comodinar los primeros dos caracteres de los candidatos de cadena, por lo que los nombres de departamento de ejemplo que coincidirían con esta consulta serían CAEngOtt o USEngCal, pero no CADocOtt. Tenga en cuenta que las coincidencias de patrones distinguen entre mayúsculas y minúsculas.
Si la cadena de patrón contiene un guion bajo o un signo de porcentaje que debe coincidir literalmente, se puede utilizar la cláusula ESCAPE para especificar un carácter que, al anteponerse a un carácter comodín, indica que debe tratarse literalmente:
SELECT d
FROM Departamento d
WHERE d.nombre LIKE'QA\_%'ESCAPE'\'
Al escapar el guion bajo, se convierte en una parte obligatoria de la expresión. Por ejemplo, QA_East coincidiría, pero QANorth no lo haría.
Subconsultas
Las subconsultas se pueden utilizar en las cláusulas WHERE y HAVING de una consulta. Una subconsulta es una consulta de selección completa dentro de un par de paréntesis que está incrustada dentro de una expresión condicional. Los resultados de ejecutar la subconsulta (que será un resultado escalar o una colección de valores) se evalúan luego en el contexto de la expresión condicional. Las subconsultas son una técnica poderosa para resolver los escenarios de consulta más complejos.
Considere la siguiente consulta:
SELECT e
FROM Empleado e
WHERE e.salario = (SELECTMAX(emp.salario)
FROM Empleado emp)
Esta consulta devuelve el empleado con el salario más alto entre todos los empleados. Se utiliza una subconsulta que consiste en una consulta de agregado (descrita más adelante en este capítulo) para devolver el valor máximo del salario, y luego este resultado se utiliza como clave para filtrar la lista de empleados por salario. Una subconsulta se puede utilizar en la mayoría de las expresiones condicionales y puede aparecer en el lado izquierdo o derecho de una expresión.
El alcance de un nombre de variable de identificación comienza en la consulta donde se define y se extiende hacia abajo en cualquier subconsulta. Las identificadores en la consulta principal pueden ser referenciados por una subconsulta, y los identificadores introducidos por una subconsulta pueden ser referenciados por cualquier subconsulta que cree. Si una subconsulta declara un identificador de variable con el mismo nombre, anula la declaración principal y evita que la subconsulta haga referencia a la variable principal.
Nota: No se garantiza que la anulación de un nombre de variable de identificación en una subconsulta sea compatible con todos los proveedores. Se deben usar nombres únicos para garantizar la portabilidad.
La capacidad de hacer referencia a una variable desde la consulta principal en la subconsulta permite que ambas consultas estén correlacionadas. Considere el siguiente ejemplo:
SELECT e
FROM Empleado e
WHEREEXISTS (SELECT1FROM Telefono p
WHERE p.empleado = e AND p.tipo ='Móvil')
Esta consulta devuelve a todos los empleados que tienen un número de teléfono celular. Esto también es un ejemplo de una subconsulta que devuelve una colección de valores. La expresión EXISTS en este ejemplo devuelve true si la subconsulta devuelve algún resultado. Devolver el literal 1 desde la subconsulta es una práctica estándar con expresiones EXISTS porque los resultados reales seleccionados por la subconsulta no importan; solo es relevante el número de resultados.
Ten en cuenta que la cláusula WHERE de la subconsulta hace referencia a la variable de identificación e de la consulta principal y la utiliza para filtrar los resultados de la subconsulta. Conceptualmente, se puede pensar que la subconsulta se ejecuta una vez por cada empleado. En la práctica, muchos servidores de bases de datos optimizarán este tipo de consultas en uniones o vistas en línea para maximizar el rendimiento.
Esta consulta también podría haberse escrito utilizando una unión entre las entidades Empleado y Telefono con el operador DISTINCT utilizado para filtrar los resultados. La ventaja de usar la subconsulta correlacionada es que la consulta principal permanece libre de uniones con otras entidades. Con frecuencia, si se utiliza una unión solo para filtrar los resultados, existe una condición de subconsulta equivalente que alternativamente se puede utilizar para eliminar restricciones en la cláusula JOIN de la consulta principal o incluso para mejorar el rendimiento de la consulta.
La cláusula FROM de una subconsulta también puede crear nuevas variables de identificación a partir de expresiones de ruta utilizando una variable de identificación de la consulta principal. Por ejemplo, la consulta anterior también podría haberse escrito de la siguiente manera:
SELECT e
FROM Empleado e
WHEREEXISTS (SELECT1FROM e.telefonos p
WHERE p.tipo ='Móvil')
En esta versión de la consulta, la subconsulta utiliza la ruta de la asociación de colecciones telefonos desde la variable de identificación del empleado e en la subconsulta. Luego, esto se asigna a una variable de identificación local p que se utiliza para filtrar los resultados por tipo de teléfono. Cada ocurrencia de p se refiere a un solo teléfono asociado al empleado.
Para ilustrar mejor cómo el traductor maneja esta consulta, considera la consulta equivalente escrita en SQL:
SELECT e.id, e.nombre, e.salario, e.idGestor, e.idDepartamento, e.idDireccion
FROM emp e
WHEREEXISTS (SELECT1FROM telefono p
WHERE p.idEmpleado = e.id ANDp.tipo ='Móvil')
La expresión e.telefonos se convierte en la tabla mapeada por la entidad Telefono. La cláusula WHERE para la subconsulta luego agrega la condición de unión necesaria para correlacionar la subconsulta con la consulta principal, en este caso, la expresión p.idEmpleado = e.id. Los criterios de unión aplicados a la tabla PHONE dan como resultado todos los teléfonos propiedad del empleado relacionado.
Expresiones IN
La expresión IN se puede utilizar para verificar si una expresión de ruta de valor único es un miembro de una colección. La colección se puede definir en línea como un conjunto de valores literales o se puede derivar de una subconsulta. La siguiente consulta demuestra la notación literal al seleccionar a todos los empleados que viven en Nueva York o California:
SELECT e
FROM Empleado e
WHERE e.direccion.provincia IN ('CO', 'PO')
La forma de subconsulta de la expresión es similar, reemplazando la lista literal con una consulta anidada. La siguiente consulta devuelve empleados que trabajan en departamentos que contribuyen a proyectos que comienzan con el prefijo QA:
SELECT e
FROM Empleado e
WHERE e.departamento IN (SELECTDISTINCT d
FROM Departamento d JOIN d.empleados de JOINde.proyectos p
WHERE p.nombre LIKE'De%')
La expresión IN también se puede negar utilizando el operador NOT. Por ejemplo, la siguiente consulta devuelve todas las entidades Telefono que representan números de teléfono que no son para la oficina ni para el hogar:
SELECT p
FROM Telefono p
WHERE p.tipo NOTIN ('Oficina', 'Casa')
Expresiones de Colecciones
El operador IS EMPTY es el equivalente lógico de IS NULL, pero para colecciones. Las consultas pueden usar el operador IS EMPTY o su forma negada IS NOT EMPTY para verificar si una ruta de asociación de colecciones se resuelve a una colección vacía o tiene al menos un valor. Por ejemplo, la siguiente consulta devuelve todos los empleados que son gerentes por tener al menos un informe directo:
SELECT e
FROM Empleado e
WHERE e.directos ISNOT EMPTY
Ten en cuenta que las expresiones IS EMPTY se traducen a SQL como expresiones de subconsultas. El traductor de consultas puede usar una subconsulta de agregado o usar la expresión SQL EXISTS. Por lo tanto, la siguiente consulta es equivalente a la anterior:
SELECT m
FROM Empleado m
WHERE (SELECTCOUNT(e)
FROM Empleado e
WHERE e.jefe = m) >0
El operador MEMBER OF y su forma negada NOT MEMBER OF son una forma abreviada de verificar si una entidad es un miembro de una ruta de asociación de colecciones. La siguiente consulta devuelve todos los gerentes que están incorrectamente registrados como informando a sí mismos:
SELECT e
FROM Empleado e
WHERE e MEMBER OF e.directos
Un uso más típico del operador MEMBER OF es en conjunción con un parámetro de entrada. Por ejemplo, la siguiente consulta selecciona a todos los empleados que están asignados a un proyecto especificado:
SELECT e
FROM Empleado e, Proyecto p
WHERE p = :proyectoSeleccionado AND e MEMBER OF p.empleados
EXISTS
La condición EXISTS devuelve true si una subconsulta devuelve alguna fila. Se mostraron ejemplos de EXISTS anteriormente en la introducción a las subconsultas. El operador EXISTS también se puede negar con el operador NOT. La siguiente consulta selecciona a todos los empleados que no tienen un teléfono celular:
SELECT e
FROM Empleado e
WHERENOTEXISTS (SELECT p
FROM e.telefonos p
WHERE p.tipo ='Móvil')
ANY, ALL y SOME
Los operadores ANY, ALL y SOME se pueden utilizar para comparar una expresión con los resultados de una subconsulta. Considera el siguiente ejemplo:
SELECT e
FROM Empleado e
WHERE e.directos ISNOT EMPTY ANDe.salario <ALL (SELECT d.salario
FROM e.directos d)
Esta consulta devuelve los gerentes que ganan menos que todos los empleados que trabajan para ellos. La subconsulta se evalúa y luego se compara cada valor de la subconsulta con la expresión izquierda, en este caso, el salario del gerente. Cuando se usa el operador ALL, la comparación entre el lado izquierdo de la ecuación y todos los resultados de la subconsulta debe ser verdadera para que la condición general sea verdadera.
El operador ANY se comporta de manera similar, pero la condición general es verdadera siempre y cuando al menos una de las comparaciones entre la expresión y el resultado de la subconsulta sea verdadera. Por ejemplo, si se especificara ANY en lugar de ALL en el ejemplo anterior, el resultado de la consulta sería todos los gerentes que ganaban menos que al menos uno de sus empleados. El operador SOME es un alias para el operador ANY.
Hay simetría entre las expresiones IN y el operador ANY. Considera la siguiente variación del ejemplo anterior del departamento de proyectos, modificado ligeramente para usar ANY en lugar de IN:
SELECT e
FROM Empleado e
WHERE e.departamento =ANY (SELECTDISTINCT d
FROM Departamento d JOIN d.empleados de
JOIN de.proyectos p
WHERE p.nombre LIKE'De%')
3.4. Herencia y Polimorfismo
Jakarta Persistence admite la herencia entre entidades. Como resultado, el lenguaje de consultas admite resultados polimórficos donde se pueden devolver múltiples subclases de una entidad mediante la misma consulta.
En el modelo de ejemplo, Proyecto es una clase base para ProyectoDesarrollo y ProyectoDocumentacion. Si se forma una variable de identificación a partir de la entidad Proyecto, los resultados de la consulta incluirán una mezcla de objetos Proyecto, ProyectoDesarrollo y ProyectoDocumentacion, y los resultados se pueden convertir a las subclases según sea necesario. La siguiente consulta recupera todos los proyectos con al menos un empleado:
SELECT p
FROM Proyecto p
WHERE p.empleados ISNOT EMPTY
3.4.1. Discriminación de Subclases
Si queremos restringir el resultado de la consulta a una subclase particular, podemos utilizar esa subclase específica en la cláusula FROM en lugar de la raíz. Sin embargo, si queremos restringir los resultados a más de una subclase en la consulta pero no a todas las subclases, podemos usar una expresión de tipo en la cláusula WHERE para filtrar los resultados. Una expresión de tipo consiste en la palabra clave TYPE seguida de una expresión entre paréntesis que se resuelve en una entidad. El resultado de una expresión de tipo es el nombre de la entidad, que luego se puede utilizar para comparaciones de tipo. La ventaja de una expresión de tipo es que podemos distinguir entre tipos sin depender de un mecanismo de discriminación en el propio modelo de dominio.
El siguiente ejemplo demuestra el uso de una expresión TYPE para devolver solo proyectos de diseño y calidad:
SELECT p
FROM Proyecto p
WHERETYPE(p) = ProyectoDocumentacion ORTYPE(p) = ProyectoDesarrollo
Ten en cuenta que no hay comillas alrededor de los identificadores ProyectoDocumentacion y ProyectoDesarrollo. Estos se tratan como nombres de entidad en Jakarta Persistence QL, no como cadenas. A pesar de esta distinción, los parámetros de entrada se pueden usar en lugar de nombres codificados en las cadenas de consulta. Crear una consulta parametrizada que devuelva instancias de un tipo de subclase dado es sencillo, como se ilustra en la siguiente consulta:
SELECT p
FROM Proyecto p
WHERETYPE(p) = :projectType
3.4.2. Downcasting
En la mayoría de los casos, al menos una de las subclases contiene algún estado adicional, como el atributo clasificacion en ProyectoDesarrollo. Un atributo de subclase se puede acceder directamente si la consulta abarca solo las entidades de la subclase, pero cuando la consulta abarca una superclase, se debe utilizar la conversión descendente (downcasting). La conversión descendente es la técnica de hacer que una expresión que se refiere a una superclase se aplique a una subclase específica. Se logra mediante el uso del operador TREAT.
TREAT se puede usar en la cláusula WHERE para filtrar los resultados según el estado del subtipo de las instancias. La siguiente consulta devuelve todos los proyectos de diseño más todos los proyectos de calidad que tienen una calificación de calidad mayor a 4:
SELECT p
FROM Proyecto p
WHERETREAT(p AS ProyectoDesarrollo).clasificacion >4ORTYPE(p) = ProyectoDocumentacion
La sintaxis de la expresión comienza con la palabra clave TREAT, seguida de su argumento entre paréntesis. El argumento es una expresión de ruta, seguida de la palabra clave AS y luego del nombre de entidad del subtipo objetivo. La expresión de ruta debe resolverse a una superclase del tipo objetivo. La expresión de conversión descendente resultante se resuelve al subtipo objetivo, por lo que se pueden agregar cualquiera de los atributos específicos del subtipo a la expresión de ruta resultante, al igual que se hizo con clasificacion en el ejemplo.
Se pueden incluir varias expresiones TREAT en la cláusula WHERE, cada una haciendo la conversión descendente al mismo o a un tipo de entidad diferente.
Normalmente, cuando se realiza una unión, incluye todas las subclases del tipo de entidad objetivo en la relación que se está uniendo. Para limitar la unión y considerar solo una jerarquía de subclases específica, se puede usar una expresión TREAT en la cláusula FROM. Asignarle un identificador proporciona la ventaja adicional de que el identificador se puede referenciar tanto en la cláusula WHERE como en la cláusula SELECT. La siguiente consulta devuelve todos los empleados que trabajan en proyectos de calidad con una calificación de calidad mayor a 4, más el nombre del proyecto en el que trabajan y su calificación de calidad:
SELECT e, q.nombre, q.clasificacion
FROM Empleado e JOINTREAT(e.proyectos AS ProyectoDesarrollo) q
WHERE q.clasificacion >4
La expresión TREAT se puede usar de manera similar para otros tipos de uniones, como uniones externas (outer joins) y uniones de recuperación (fetch joins).
Es importante entender el impacto que tiene la herencia entre entidades en el SQL generado por razones de rendimiento.
3.5. Expresiones escalares
Una expresión escalar es un valor literal, una secuencia aritmética, una expresión de función, una expresión de tipo o una expresión de caso que se resuelve a un solo valor escalar. Se puede utilizar en la cláusula SELECT para dar formato a los campos proyectados en consultas de informes o como parte de expresiones condicionales en la cláusula WHERE o HAVING de una consulta. Las subconsultas que se resuelven a valores escalares también se consideran expresiones escalares, pero solo se pueden usar al componer criterios en la cláusula WHERE de una consulta. Las subconsultas nunca se pueden utilizar en la cláusula SELECT.
3.5.1. Literales
Existen varios tipos de literales que se pueden utilizar en Jakarta Persistence QL, incluyendo cadenas, numéricos, booleanos, enumeraciones, tipos de entidad y tipos temporales.
A lo largo de este capítulo, hemos mostrado muchos ejemplos de literales de cadena, entero y booleano. Las comillas simples se utilizan para delimitar literales de cadena y se escapan dentro de una cadena prefijando la comilla con otra comilla simple. Los numéricos exactos y aproximados se pueden definir según las convenciones del lenguaje de programación Java o utilizando la sintaxis estándar SQL-92. Los valores booleanos se representan con los literales TRUE y FALSE.
Las consultas pueden hacer referencia a los tipos enum de Java especificando el nombre completo de la clase enum. El siguiente ejemplo demuestra el uso de un enum en una expresión condicional, utilizando el enum TipoTelefono:
SELECT e
FROM Empleado e JOIN e.numerosTelefono p
WHEREKEY(p) = com.acme.TipoTelefono.Casa
Un tipo de entidad es simplemente el nombre de entidad de alguna entidad definida y es válido solo cuando se usa con el operador TYPE. Las comillas no se utilizan. Consulte la sección “Herencia y polimorfismo” para ver ejemplos de cuándo usar un literal de tipo de entidad.
Los literales temporales se especifican utilizando la sintaxis de escape JDBC, que define que las llaves encierran el literal. El primer carácter en la secuencia es un “d” o un “t” para indicar que el literal es una fecha o una hora, respectivamente. Si el literal representa una marca de tiempo, se utiliza “ts”. Después del indicador de tipo hay un separador de espacio y luego la fecha real, la hora o la información de la marca de tiempo envuelta entre comillas simples. Las formas generales de los tres tipos de literales temporales, con ejemplos acompañantes, son las siguientes:
{d ‘yyyy-mm-dd’} p. ej., {d ‘2009-11-05’}
{t ‘hh-mm-ss’} p. ej., {t ‘12-45-52’}
{ts ‘yyyy-mm-dd hh-mm-ss.f’} p. ej., {ts ‘2009-11-05 12-45-52.325’}
Toda la información temporal dentro de comillas simples se expresa como dígitos. La parte fraccionaria de la marca de tiempo (la parte “.f”) puede tener varios dígitos y es opcional. Al usar cualquiera de estos literales temporales, recuerde que solo son interpretados por los controladores que admiten la sintaxis de escape JDBC. Normalmente, el proveedor no intentará traducir ni procesar los literales temporales.
3.5.2. Function Expressions
Las expresiones escalares pueden aprovechar funciones que se pueden utilizar para transformar los resultados de la consulta. La Tabla 8-1 resume la sintaxis de cada una de las expresiones de funciones admitidas.
Función
Descripción
ABS(numero)
Devuelve la versión no firmada del argumento numero. El tipo de resultado es el mismo que el tipo de argumento (entero, float o double).
CONCAT(string1, string2)
Devuelve una nueva cadena que es la concatenación de sus argumentos, string1 y string2.
CURRENT_DATE
Devuelve la fecha actual según lo definido por el servidor de base de datos.
CURRENT_TIME
Devuelve la hora actual según lo definido por el servidor de base de datos.
CURRENT_TIMESTAMP
Devuelve la marca de tiempo actual según lo definido por el servidor de base de datos.
INDEX(identification variable)
Devuelve la posición de una entidad dentro de una lista ordenada.
EXTRACT(datetime_field FROM datetime_expression)
Devuelve el valor del campo o parte correspondiente de la fecha y hora.
LENGTH(string)
Devuelve el número de caracteres en el argumento de cadena.
LOCATE(string1, string2 [, start])
Devuelve la posición de string1 en string2, opcionalmente comenzando en la posición indicada por start. El resultado es cero si no se puede encontrar la cadena.
LOWER(string)
Devuelve la forma en minúsculas del argumento de cadena.
MOD(number1, number2)
Devuelve el módulo de los argumentos numéricos number1 y number2 como un entero.
SIZE(collection)
Devuelve el número de elementos en la colección, o cero si la colección está vacía.
SQRT(numero)
Devuelve la raíz cuadrada del argumento numérico como un double.
SUBSTRING(string, start, end)
Devuelve una parte de la cadena de entrada, comenzando en el índice indicado por start hasta la longitud de los caracteres. Los índices de cadena se miden a partir de uno.
UPPER(string)
Devuelve la forma en mayúsculas del argumento de cadena.
Elimina caracteres iniciales y/o finales de una cadena. Si no se utiliza la palabra clave opcional LEADING, TRAILING o BOTH, se eliminan tanto los caracteres iniciales como los finales. El carácter de recorte predeterminado es el espacio.
La función SIZE requiere atención especial porque es una notación abreviada para una subconsulta agregada. Por ejemplo, considere la siguiente consulta que devuelve todos los departamentos con solo dos empleados:
SELECT d
FROM Departamento d
WHERESIZE(d.empleados) =2
Al igual que las expresiones de colección IS EMPTY y MEMBER OF, la función SIZE se traducirá a SQL utilizando una subconsulta. La forma equivalente del ejemplo anterior usando una subconsulta es la siguiente:
SELECT d
FROM Departamento d
WHERE (SELECTCOUNT(e)
FROM d.empleados e) =2
El caso de uso para la función INDEX puede no ser obvio al principio. Cuando se utilizan colecciones ordenadas, cada elemento de la colección contiene dos piezas de información: el valor almacenado en la colección y su posición numérica dentro de la colección. Las consultas pueden usar la función INDEX para determinar la posición numérica de un elemento en una colección y luego usar ese número con fines de informes o filtrado. Por ejemplo, si los números de teléfono de un empleado se almacenan en orden de prioridad, la siguiente consulta devolvería el primer (y más importante) número para cada empleado:
SELECT e.nombre, p.numero
FROM Empleado e JOIN e.telefonos p
WHEREINDEX(p) =0
3.5.3. Funciones Nativas de la base de datos: FUNCTION
Uno de los beneficios de Jakarta Persistence QL es que desacopla la aplicación de la base de datos subyacente. Sin embargo, ocasionalmente es necesario utilizar funciones nativas que son propias de la base de datos o definidas por el administrador del sistema. Si bien el uso de estas funciones puede vincular la consulta a la base de datos de destino, aún hay un argumento para utilizar la independencia del mapeo de entidades de Jakarta Persistence QL.
Las funciones de base de datos pueden ser ejecutarse en las consultas de Jakarta Persistence QL a través del uso de la expresión FUNCTION. La palabra clave FUNCTION, seguida del nombre de la función y los argumentos de la función, debe resolverse a un valor escalar que sea aritmético, booleano, de cadena o de un tipo temporal, como fecha, hora o marca de tiempo. Las expresiones FUNCTION pueden ser utilizadas donde sea que los tipos escalares encajen en una expresión, y el tipo de resultado debe coincidir con lo que el resto de la expresión espera. Los argumentos deben ser literales, expresiones que se resuelvan a escalares o parámetros de entrada.
La siguiente consulta invoca una función de base de datos llamada deberiaTenerBonus. El ID del departamento del empleado y los proyectos en los que trabaja se pasan como parámetros, y el tipo de retorno de la función es un booleano. El resultado crea una condición que hace que la consulta devuelva el conjunto de todos los empleados que obtienen un bono:
SELECTDISTINCT e
FROM Empleado e JOIN e.proyectos p
WHEREFUNCTION('deberiaTenerBonus', e.dept.id, p.id)
3.5.4. Expresiones CASE
La expresión CASE de Jakarta Persistence QL es una adaptación de la expresión CASE ANSI SQL-92, teniendo en cuenta las capacidades del lenguaje Jakarta Persistence QL. Las expresiones CASE son herramientas poderosas para introducir lógica condicional en una consulta, con el beneficio de que el resultado de una expresión CASE se puede utilizar en cualquier lugar donde sea válida una expresión escalar.
Las expresiones CASE están disponibles en cuatro formas, dependiendo de la flexibilidad requerida por la consulta. La primera y más flexible forma es la expresión de caso general. Todos los demás tipos de expresión CASE se pueden componer en términos de la expresión de caso general. Tiene la siguiente forma:
El corazón de la expresión CASE es la cláusula WHEN, de la cual debe haber al menos una. El procesador de consultas resuelve la expresión condicional de cada cláusula WHEN en orden hasta que encuentra una que tenga éxito. Luego evalúa la expresión escalar para esa cláusula WHEN y la devuelve como resultado de la expresión CASE. Si ninguna de las expresiones condicionales de la cláusula WHEN produce un resultado verdadero, se evalúa y devuelve la expresión escalar de la cláusula ELSE. El siguiente ejemplo muestra la expresión de caso general, enumerando el nombre y tipo de cada proyecto que tiene empleados asignados:
SELECT p.nombre,
CASEWHENTYPE(p) = ProyectoDocumentacion THEN'Desarrollo'WHENTYPE(p) = ProyectoDesarrollo THEN'Desarrollo'ELSE'No-Desarrollo'ENDFROM Proyecto p
WHERE p.empleados ISNOT EMPTY
Observe el uso de la expresión de caso como parte de la cláusula select. Las expresiones CASE son una herramienta poderosa para transformar datos de entidades en consultas de informes.
Una ligera variación en la expresión de caso general es la expresión de caso simple. En lugar de verificar una expresión condicional en cada cláusula WHEN, identifica un valor y resuelve una expresión escalar en cada cláusula WHEN. El primero en coincidir con el valor activa una segunda expresión escalar que se convierte en el valor de la expresión de caso. Tiene la siguiente forma:
El <value> en esta forma de la expresión es ya sea una expresión de ruta que lleva a un campo de estado o una expresión de tipo para una comparación polimórfica. Podemos simplificar el último ejemplo convirtiéndolo en una expresión de caso simple:
SELECT p.nombre,
CASETYPE(p)
WHEN ProyectoDocumentacion THEN'Desarrollo'WHEN ProyectoDesarrollo THEN'Desarrollo'ELSE'Non-Desarrollo'ENDFROM Proyecto p
WHERE p.empleados ISNOT EMPTY
La tercera forma de la expresión de caso es la expresión coalesce. Esta forma de la expresión de caso acepta una secuencia de una o más expresiones escalares. Tiene la siguiente forma:
COALESCE(<scalar_expr>{,<scalar_expr>}+)
Las expresiones escalares en la expresión COALESCE se resuelven en orden. La primera que devuelve un valor no nulo se convierte en el resultado de la expresión. El siguiente ejemplo demuestra este uso, devolviendo ya sea el nombre descriptivo de cada departamento o el identificador del departamento si no se ha definido ningún nombre:
SELECT COALESCE(d.nombre, d.id)
FROM Departamento d
La cuarta y última forma de la expresión de caso es algo inusual. Acepta dos expresiones escalares y resuelve ambas. Si los resultados de las dos expresiones son iguales, el resultado de la expresión es nulo. De lo contrario, devuelve el resultado de la primera expresión escalar. Esta forma de la expresión de caso se identifica por la palabra clave NULLIF:
NULLIF(<scalar_expr1>, <scalar_expr2>)
Un truco útil con NULLIF es excluir resultados de una función de agregación. Por ejemplo, la siguiente consulta devuelve un recuento de todos los departamentos y un recuento de todos los departamentos que no tienen el nombre ‘QA’:
SELECTCOUNT(*), COUNT(NULLIF(d.nombre, 'QA'))
FROM Departamento d
Si el nombre del departamento es ‘QA’, NULLIF devolverá NULL, lo cual luego será ignorado por la función COUNT. Las funciones de agregación ignoran los valores NULL, y se describen más adelante en la sección “Consultas de Agregación”.
3.6. Cláusula ORDER BY
Las consultas pueden ordenarse opcionalmente mediante ORDER BY y una o más expresiones que consisten en variables de identificación, variables de resultado, una expresión de ruta que resuelve a una sola entidad o una expresión de ruta que resuelve a un campo de estado persistente. Las palabras clave opcionales ASC y DESC después de la expresión se pueden usar para indicar órdenes ascendentes o descendentes, respectivamente. El orden de clasificación predeterminado es ascendente.
El siguiente ejemplo demuestra la clasificación por un solo campo:
SELECT e
FROM Empleado e
ORDERBY e.nombre DESC
También se pueden usar múltiples expresiones para refinar el orden de clasificación:
SELECT e, d
FROM Empleado e JOIN e.departamento d
ORDERBY d.nombre, e.nombre DESC
Se puede declarar una variable de resultado en la cláusula SELECT con el propósito de especificar un ítem que se ordenará. Una variable de resultado es efectivamente un alias para su ítem de selección asignado. Ahorra a la cláusula ORDER BY tener que duplicar expresiones de ruta de la cláusula SELECT y permite hacer referencia a ítems de selección calculados e ítems que utilizan funciones de agregación. La siguiente consulta define dos variables de resultado en la cláusula SELECT y luego las utiliza para ordenar los resultados en la cláusula ORDER BY:
SELECT e.nombre, e.salario *0.05AS bonus, d.nombre AS nomberDepartamento
FROM Empleado e JOIN e.departamento d
ORDERBY nomberDepartamento, bonus DESC
Si la cláusula SELECT de la consulta utiliza expresiones de ruta de campo de estado, la cláusula ORDER BY se limita a las mismas expresiones de ruta utilizadas en la cláusula SELECT. Por ejemplo, la siguiente consulta no es legal:
SELECT e.nombre
FROM Empleado e
ORDERBY e.salario DESC
Debido a que el tipo de resultado de la consulta es el nombre del empleado, que es de tipo String, el resto de los campos de estado de Empleado ya no están disponibles para ordenar.
4. Consultas de Agregación
Una consulta de agregación es una variación de una consulta select normal. Una consulta de agregación agrupa resultados y aplica funciones de agregación para obtener información resumida sobre los resultados de la consulta. Una consulta se considera una consulta de agregación si utiliza una función de agregación o posee una cláusula GROUP BY y / o una cláusula HAVING. La forma más típica de consulta de agregación implica el uso de una o más expresiones de agrupación y funciones de agregación en la cláusula SELECT emparejadas con expresiones de agrupación en la cláusula GROUP BY. La sintaxis de una consulta de agregación es la siguiente:
Las cláusulas SELECT, FROM y WHERE se comportan de manera similar a como se describió anteriormente en las consultas select, con la excepción de algunas restricciones sobre cómo se formula la cláusula SELECT.
El poder de una consulta de agregación proviene del uso de funciones de agregación sobre datos agrupados. Considera el siguiente ejemplo de agregación simple:
SELECTAVG(e.salario)
FROM Empleado e
Esta consulta devuelve el salario promedio de todos los empleados de la empresa. AVG es una función de agregación que toma una expresión de ruta de campo de estado numérico como argumento y calcula el promedio sobre el grupo. Como no se especificó ninguna cláusula GROUP BY, el grupo aquí es el conjunto completo de empleados.
Ahora considera esta variación, donde el resultado se ha agrupado por el nombre del departamento:
SELECT d.nombre, AVG(e.salario)
FROM Departamento d JOIN d.empleados e
GROUPBY d.nombre
Esta consulta devuelve el nombre de cada departamento y el salario promedio de los empleados en ese departamento. La entidad Departamento se une a la entidad Empleado a través de la relación empleados y luego se forma en un grupo definido por el nombre del departamento. La función AVG luego calcula su resultado en función de los datos de empleados en este grupo.
Esto se puede extender aún más para filtrar los datos de modo que los salarios de los gerentes no se incluyan:
SELECT d.nombre, AVG(e.salario)
FROM Departamento d JOIN d.empleados e
WHERE e.directos IS EMPTY
GROUPBY d.nombre
Finalmente, podemos extender esto una última vez para devolver solo los departamentos donde el salario promedio es mayor a $50,000. Considera la siguiente versión de la consulta anterior:
SELECT d.nombre, AVG(e.salario)
FROM Departamento d JOIN d.empleados e
WHERE e.directos IS EMPTY
GROUPBY d.nombre
HAVINGAVG(e.salario) >50000
Para comprender mejor esta consulta, revisemos los pasos lógicos que se llevaron a cabo para ejecutarla. Las bases de datos utilizan muchas técnicas para optimizar este tipo de consultas, pero conceptualmente se sigue el mismo proceso.
Primero, se ejecuta la siguiente consulta sin agrupación:
SELECT d.nombre, e.salario
FROM Departamento d JOIN d.empleados e
WHERE e.directos IS EMPTY
Esto producirá un conjunto de resultados que consiste en todos los pares de nombre de departamento y valor de salario. El motor de consulta luego inicia un nuevo conjunto de resultados y realiza un segundo paso sobre los datos, recopilando todos los valores de salario para cada nombre de departamento y entregándolos a la función AVG. Esta función devuelve el promedio del grupo, que luego se verifica contra los criterios de la cláusula HAVING. Si el valor promedio es mayor que $50,000, el motor de consulta genera una fila de resultados que consiste en el nombre del departamento y el valor promedio del salario.
Las siguientes secciones describen las funciones de agregación disponibles para su uso en consultas de agregación y el uso de las cláusulas GROUP BY y HAVING.
4.1 Funciones de Agregación
AVG
La función AVG toma una expresión de ruta de campo de estado como argumento y calcula el valor promedio de ese campo de estado sobre el grupo. El tipo de campo de estado debe ser numérico y el resultado se devuelve como un Double.
COUNT
La función COUNT toma ya sea una variable de identificación o una expresión de ruta como argumento. Esta expresión de ruta puede resolverse a un campo de estado o un campo de asociación de valor único. El resultado de la función es un valor Long que representa la cantidad de valores en el grupo. El argumento de la función COUNT puede ir precedido opcionalmente por la palabra clave DISTINCT, en cuyo caso se eliminan los valores duplicados antes de contar.
Ejemplo: Contar la cantidad de teléfonos asociados con cada empleado y la cantidad de tipos de números distintos (celular, oficina, hogar, etc.).
SELECT e, COUNT(p), COUNT(DISTINCT p.tipo)
FROM Empleado e JOIN e.telefonos p
GROUPBY e;
MAX
La función MAX toma una expresión de campo de estado como argumento y devuelve el valor máximo en el grupo para ese campo de estado.
MIN
La función MIN toma una expresión de campo de estado como argumento y devuelve el valor mínimo en el grupo para ese campo de estado.
SUM
La función SUM toma una expresión de campo de estado como argumento y calcula la suma de los valores en ese campo de estado sobre el grupo. El tipo de campo de estado debe ser numérico y el tipo de resultado debe corresponder al tipo de campo. Por ejemplo, si se suma un campo Double, el resultado se devolverá como un Double. Si se suma un campo Long, la respuesta se devolverá como un Long.
4.2 Cláusula GROUP BY
La cláusula GROUP BY define las expresiones de agrupación sobre las cuales se agregarán los resultados. Una expresión de agrupación debe ser una expresión de ruta de campo de valor único (campo de estado, embebido que lleva a un campo de estado o campo de asociación de valor único) o una variable de identificación. Si se utiliza una variable de identificación, la entidad no debe tener ningún campo de estado serializado u objetos grandes.
El siguiente ejemplo cuenta la cantidad de empleados en cada departamento:
SELECT d.nombre, COUNT(e)
FROM Departamento d JOIN d.empleados e
GROUPBY d.nombre;
Observa que la misma expresión de campo utilizada en la cláusula SELECT se repite en la cláusula GROUP BY. Todas las expresiones que no son de agregación deben enumerarse de esta manera.
Es posible aplicar más de una función de agregación, como se muestra en el siguiente ejemplo:
SELECT d.nombre, COUNT(e), AVG(e.salario)
FROM Departamento d JOIN d.empleados e
GROUPBY d.nombre;
En esta variación de la consulta, se calcula el salario promedio de todos los empleados en cada departamento además de contar la cantidad de empleados en el departamento.
También se pueden usar múltiples expresiones de agrupación para desglosar aún más los resultados:
SELECT d.nombre, e.salario, COUNT(p)
FROM Departamento d JOIN d.empleados e JOIN e.proyectos p
GROUPBY d.nombre, e.salario;
Ambas expresiones de agrupación, el nombre del departamento y el salario del empleado, deben enumerarse tanto en la cláusula SELECT como en la cláusula GROUP BY. Para cada departamento, esta consulta cuenta la cantidad de proyectos asignados a los empleados según su salario.
En ausencia de una cláusula GROUP BY, las funciones de agregación se aplicarán a todo el conjunto de resultados como un único grupo. Por ejemplo, la siguiente consulta devuelve la cantidad de empleados y su salario promedio en toda la empresa:
SELECTCOUNT(e), AVG(e.salario)
FROM Empleado e;
4.3 Cláusula HAVING
La cláusula HAVING define un filtro que se aplicará después de que los resultados de la consulta hayan sido agrupados. Es efectivamente una cláusula WHERE secundaria, y su definición es la misma: la palabra clave HAVING seguida de una expresión condicional. La diferencia clave con la cláusula HAVING es que sus expresiones condicionales están principalmente limitadas a campos de estado o campos de asociación de valor único incluidos en el grupo.
Las expresiones condicionales en la cláusula HAVING también pueden usar funciones de agregación sobre los elementos utilizados para la agrupación, o funciones de agregación que aparecen en la cláusula SELECT. En muchos aspectos, el uso principal de la cláusula HAVING es restringir los resultados basados en los valores de resultado agregados. La siguiente consulta utiliza esta técnica para recuperar todos los empleados asignados a dos o más proyectos:
SELECT e, COUNT(p)
FROM Empleado e JOIN e.proyectos p
GROUPBY e
HAVINGCOUNT(p) >=2;
5. Consultas de Actualización
Las consultas de actualización proporcionan un equivalente a la instrucción SQL UPDATE pero con expresiones condicionales de Jakarta Persistence QL. La forma de una consulta de actualización es la siguiente:
UPDATE<nombre de entidad> [[AS] <variable de identificación>]
SET<declaración de actualización>{, <declaración de actualización>}*[WHERE<expresión condicional>]
Cada declaración UPDATE consiste en una expresión de ruta de valor único, el operador de asignación (=) y una expresión. Las opciones de expresión para la declaración de asignación están ligeramente restringidas en comparación con las expresiones condicionales regulares. El lado derecho de la asignación debe resolverse a un literal, una expresión simple que resuelva a un tipo básico, una expresión de función, una variable de identificación o un parámetro de entrada. El tipo de resultado de esa expresión debe ser compatible con la ruta de asociación simple o el campo de estado persistente en el lado izquierdo de la asignación.
El siguiente ejemplo simple demuestra la consulta de actualización al darle un aumento a $60,000 a los empleados que ganan $55,000:
UPDATE Empleado e
SET e.salario =60000WHERE e.salario =55000;
La cláusula WHERE de una declaración UPDATE funciona de la misma manera que una declaración SELECT y puede utilizar la variable de identificación definida en la cláusula UPDATE en expresiones. Una consulta de actualización ligeramente más compleja pero más realista sería otorgar un aumento de $5,000 a los empleados que trabajaron en un proyecto en particular:
UPDATE Empleado e
SET e.salario = e.salario +5000WHEREEXISTS (SELECT p
FROM e.proyectos p
WHERE p.nombre ='Versión2');
Se pueden modificar más de una propiedad de la entidad objetivo con una sola declaración UPDATE. Por ejemplo, la siguiente consulta actualiza el intercambio telefónico para los empleados en la ciudad de Ottawa y cambia la terminología del tipo de teléfono de Oficina a Negocios:
UPDATE Telefono p
SET p.numero = CONCAT('288', SUBSTRING( p.numero,
LOCATE('-', p.numero), 4)),
p.tipo ='Business'WHERE p.empleado.direccion.city ='Ottawa'ANDp.tipo ='Oficina';
6. Consultas de borrado
La consulta de eliminación proporciona la misma capacidad que la instrucción SQL DELETE, pero con expresiones condicionales de Jakarta Persistence QL. La forma de una consulta de eliminación es la siguiente:
DELETEFROM<nombre de entidad> [[AS] <variable de identificación>]
[WHERE<condición>]
El siguiente ejemplo elimina a todos los empleados que no están asignados a un departamento:
DELETEFROM Empleado e
WHERE e.departamento ISNULL;
La cláusula WHERE de una declaración DELETE funciona de la misma manera que lo haría para una declaración SELECT. Todas las expresiones condicionales están disponibles para filtrar el conjunto de entidades que se eliminarán. Si no se proporciona la cláusula WHERE, se eliminan todas las entidades del tipo dado.
Las consultas de eliminación son polimórficas. Todas las instancias de subclases de entidades que cumplan con los criterios de la consulta de eliminación también se eliminarán. Sin embargo, las consultas de eliminación no respetan las reglas de cascada. No se eliminarán entidades que no sean del tipo referenciado en la consulta y sus subclases, incluso si la entidad tiene relaciones con otras entidades con eliminaciones en cascada habilitadas.
Las grandes cantidades de resultados de consultas suelen ser un problema para muchas aplicaciones.
Cuando mostramos el conjunto completo de resultados, por que son muchos, o si el medio de la aplicación hace que mostrar muchas filas sea ineficiente (aplicaciones web, en particular), las aplicaciones deben poder mostrar rangos de un conjunto de resultados y proporcionar a los usuarios la capacidad de controlar el rango de datos que están visualizando.
paginación
La forma más común de esta técnica es presentar al usuario una tabla de tamaño fijo que actúa como una ventana deslizante sobre el conjunto de resultados (en tablas o páginas Web, por ejemplo). Cada incremento de resultados mostrados se llama página, y el proceso de navegar a través de los resultados se llama paginación.
Paginar eficientemente a través de conjuntos de resultados ha sido un desafío tanto para los desarrolladores de aplicaciones como para los proveedores de bases de datos.
Antes de que existiera soporte a nivel de base de datos, una técnica común era recuperar primero todas las claves primarias del conjunto de resultados y luego emitir consultas separadas para obtener los resultados completos utilizando rangos de valores de clave primaria.
Más tarde, los proveedores de bases de datos agregaron el concepto de número de fila lógica a los resultados de las consultas, garantizando que mientras el resultado estuviera ordenado, se podría confiar en el número de fila para recuperar porciones del conjunto de resultados (Ejemplo: SELECT * FROM posts OFFSET 10 LIMIT 10.).
Más recientemente, la especificación JDBC ha llevado esto aún más lejos con el concepto de conjuntos de resultados desplazables, que se pueden navegar hacia adelante y hacia atrás según sea necesario.
La fórmula para el cálculo de la paginación es la siguiente:
OFFSET = (LIMIT * page(N)) — LIMIT
int primerResultado = (nuMeroPágina - 1) * tamanhoPagina;
Las interfaces Query y TypedQuery brindan soporte para la paginación a través de los métodos setFirstResult() y setMaxResults(). Estos métodos especifican el primer resultado que se recibirá (numerado desde cero) y el número máximo de resultados a devolver en relación con ese punto.
Los valores establecidos para estos métodos también se pueden recuperar mediante los métodos getFirstResult() y getMaxResults(). Un proveedor de persistencia puede optar por implementar el soporte para esta característica de varias maneras, ya que no todos los sistemas de bases de datos se benefician del mismo enfoque. Es una buena idea familiarizarse con la forma en que su proveedor aborda la paginación y qué nivel de soporte existe en la plataforma de base de datos objetivo para su aplicación.
Conjunto de resultados en relaciones multivaluadas
Los métodos setFirstResult() y setMaxResults() no deben usarse con consultas que realicen uniones a través de relaciones de colección (uno a muchos y muchos a muchos), porque estas consultas pueden devolver valores duplicados. Los valores duplicados en el conjunto de resultados hacen imposible utilizar una posición de resultado lógica.
En el siguiente código se muestra un ejemplo de paginación. Una vez creado, se inicializa con el nombre de una consulta para contar el total de resultados y el nombre de una consulta para generar el informe. Cuando se solicitan resultados, utiliza el tamaño de página y el número de página actual para calcular los parámetros correctos para los métodos setFirstResult() y setMaxResults(). El número total posible de resultados se calcula ejecutando la consulta de recuento. Usando los métodos next(), previous() y getResultadosActuales(), el código de presentación puede navegar por los resultados según sea necesario. Si este bean se vinculara a una sesión HTTP, podría usarse directamente en una página de Jakarta Server Pages o Jakarta Server Faces que presente los resultados en una tabla de datos.
La clase es una plantilla general para un bean que mantiene el estado intermedio para una consulta de aplicación a partir de la cual los resultados se procesan en segmentos.
Se utiliza un bean de sesión con estado.
URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas
URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false
Pese a todo lo mejor es que el usuario y contraseña se referencien en el archivo de propiedades, persistence.xml, de modo independiente, como se ha visto en el tema de configuración.
Las tablas de la base de datos y las entidades ya han sido desarrolladas en apartados anteriores.
Se trata de realizar una aplicación que permita consultar las películas por nombre (pide la introducción de un texto) y muestre las películas de la base de datos de 10 en 10. Se debe mostrar el idPelicula, castelan, orixinal, anoFin y el director (relacionado con PeliculaPersonaxe).
Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.
La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.
Crea una clase PeliculaPaginaDTO que tenga los campos idPelicula, castelan, orixinal, anoFin y director.
Crea una clase PeliculaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.
Uno de los errores comunes cometidos por desarrolladores novatos orientados a objetos se abusa del principio de reutilización, llevándolo demasiado lejos, dando lugar a jerarquías de herencia complejas solo por el bien de compartir algunos métodos. Este tipo de jerarquías a menudo conduce a problemas y dificultades a medida que la aplicación se vuelve difícil de depurar y un desafío de mantener.
La mayoría de las aplicaciones disfrutan de los beneficios de al menos alguna herencia en el modelo de objetos. Sin embargo, como con la mayoría de las cosas, se debe aplicar la moderación, especialmente cuando se trata de mapear las clases a bases de datos relacionales.
Grandes jerarquías a menudo pueden llevar a una reducción significativa del rendimiento, y puede ser que el costo de la reutilización de código sea más alto de lo que te gustaría pagar.
Veremos cómo la API de JPA jerarquías de herencia.
1. Jerarquías de Clases
El primer y más obvio lugar para comenzar a hablar sobre herencia es en el modelo de objetos de Java. Después de todo, las entidades son objetos y deberían poder heredar estado y comportamiento de otras entidades. Esto no solo se espera, sino que también es esencial para el desarrollo de aplicaciones orientadas a objetos.
¿Qué significa cuando una entidad hereda estado de su superclase de entidad? Puede implicar cosas diferentes en el modelo de datos, pero en el modelo de Java, simplemente significa que cuando se instancia una entidad de la subclase, tiene su propia versión o copia tanto de su estado localmente definido como de su estado heredado, todo lo cual es persistente. Si bien esta premisa básica no es sorprendente en absoluto, abre la pregunta menos obvia de qué sucede cuando una entidad hereda de algo que no es otra entidad. ¿A qué clases se le permite a una entidad heredar y qué sucede cuando lo hace?
Considera la jerarquía de clases: Empleado - EmpleadoCompañia - EmpladoTiempoCompleto/EmpleadoTiempoParcial:
classDiagram
class Empleado {
-int id
-String nombre
-Date fechaInicio
}
class EmpleadoContratado {
-int tarifaDiaria
-int plazo
}
class EmpleadoCompania {
-int vacaciones
}
class EmpleadoTiempoCompleto {
-long salario
-long pension
}
class EmpleadoTiempoParcial {
-float tarifaHoraria
}
Empleado <|-- EmpleadoContratado
Empleado <|-- EmpleadoCompania
EmpleadoCompania <|-- EmpleadoTiempoCompleto
EmpleadoCompania <|-- EmpleadoTiempoParcial
Hay varias formas en que la herencia de clases se puede representar en la base de datos. En el modelo de objetos, puede haber varias formas diferentes de implementar una jerarquía, algunas de las cuales pueden incluir clases que no son entidades.
Diferenciamos entre:
Una jerarquía de clases general, que es un conjunto de varios tipos de clases de Java que se extienden entre sí en un árbol.
Una jerarquía de entidades, que es un árbol que consiste en clases de entidades persistentes intercaladas con clases que no son entidades. Una jerarquía de entidades tiene su raíz en la primera clase de entidad en la jerarquía.
La API de Persistencia de Jakarta define un tipo especial de clase llamada superclase mapeada (@MappedSuperclass) que es bastante útil como superclase para entidades.
Una superclase mapeada proporciona una clase útil en la que almacenar estado y comportamiento compartidos que las entidades pueden heredar, pero:
En sí misma no es una clase persistente.
No puede actuar como una entidad.
No se puede consultar.
No puede ser el objetivo de una relación.
Anotaciones como @Table no están permitidas en superclases mapeadas porque el estado definido en ellas se aplica solo a sus subclases de entidad.
Las superclases mapeadas se pueden comparar con las entidades de alguna manera similar a como se compara una clase abstracta con una clase concreta; pueden contener estado y comportamiento, pero simplemente no se pueden instanciar como entidades persistentes.
Una clase abstracta solo es útil en relación con sus subclases concretas, y una superclase mapeada es útil solo como estado y comportamiento que se hereda de las subclases de entidad. No juegan un papel en una jerarquía de herencia de entidades aparte de contribuir con ese estado y comportamiento a las entidades que heredan de ellas.
Las superclases mapeadas pueden o no definirse como abstractas en sus definiciones de clase, pero es una buena práctica hacerlas clases Java abstractas reales. No conocemos casos de uso buenos para crear instancias concretas de ellas sin poder persistirlas nunca, y lo más probable es que, si encuentras uno, probablemente quieras que la superclase mapeada sea una entidad.
Todas las reglas de asignación predeterminadas que se aplican a las entidades también se aplican al estado básico y de relación en las superclases mapeadas. La mayor ventaja de usar superclases mapeadas es poder definir un estado compartido parcial que no debería accederse por sí mismo sin el estado adicional que sus subclases de entidad le agregan.
una entidad o una superclase mapeada
Si no estás seguro de si hacer una clase una entidad o una superclase mapeada, solo necesitas preguntarte si alguna vez se precisa realizar consultas o acceder a una instancia que solo se expone como una instancia de esa clase mapeada. Esto también incluye relaciones, ya que una superclase mapeada no se puede usar como el destino de una relación. Si respondes sí a alguna variante de esa pregunta, probablemente deberías hacerla una entidad de primera clase.
Volviendo a la relación anterior, podríamos concebir tratar la clase EmpleadoCompania como una superclase mapeada en lugar de una entidad. Define un estado compartido, pero quizás no tengamos ninguna razón para realizar consultas sobre ella.
Una clase se indica como una superclase mapeada anotándola con la anotación @MappedSuperclass. Los fragmentos de clase siguientes muestran cómo se mapearía la jerarquía con EmpleadoCompania como una superclase mapeada:
Las clases en una jerarquía de entidades, que no son entidades ni superclases mapeadas , se llaman clases transitorias.
Las entidades se pueden heredar clases transitorias ya sea directa o indirectamente a través de una superclase mapeada.
Cuando una entidad hereda de una clase transitoria, el estado definido en la clase transitoria aún se hereda en la entidad, pero no es persistente. En otras palabras, la entidad tendrá espacio asignado para el estado heredado, según las reglas habituales de Java, pero ese estado no será gestionado por el proveedor de persistencia. Se ignorará efectivamente durante el ciclo de vida de la entidad. La entidad podría gestionar ese estado manualmente mediante el uso de métodos de devolución de llamada del ciclo de vida u otros enfoques, pero el estado no se persistirá como parte del ciclo de vida gestionado por el proveedor.
Podría concebirse tener una jerarquía compuesta por una entidad que tiene una subclase transitoria, que a su vez tiene una o más subclases de entidad. Aunque este caso no es realmente común, es posible y se puede lograrse en las raras circunstancias en las que se desee tener un estado transitorio compartido o un comportamiento común. Normalmente, sería más conveniente declarar el estado o comportamiento transitorio en la superclase de entidad que crear una clase transitoria intermedia.
El código muestra una entidad que hereda de una superclase que define un estado transitorio, que es el tiempo en que se creó una entidad en la memoria.
En este ejemplo, movimos el estado transitorio de la clase de entidad a una superclase transitoria, pero el resultado final es realmente bastante similar. El ejemplo anterior podría haber sido un poco más limpio sin la clase adicional, pero este ejemplo nos permite compartir el estado transitorio y el comportamiento entre cualquier número de entidades que solo necesitan extender EntidadConCache.
1.3. Clases Abstractas y Concretas
Hemos mencionado la noción de clases abstractas frente a concretas en el contexto de superclases mapeadas, pero no entramos en más detalles sobre entidades y clases transitorias. La mayoría de las personas, dependiendo de su filosofía, podrían esperar que todas las clases no hoja en una jerarquía de objetos sean abstractas, o al menos algunas de ellas. Una restricción que obligue a que las entidades siempre sean clases concretas estropearía esto de manera bastante hábil, y afortunadamente este no es el caso.
Es perfectamente aceptable que las entidades, superclases mapeadas o clases transitorias sean abstractas o concretas en cualquier nivel del árbol de herencia. Al igual que con las superclases mapeadas, hacer que las clases transitorias sean concretas en la jerarquía realmente no sirve para ningún propósito y, como regla general, se debe evitar para prevenir errores de desarrollo accidentales y mal uso.
El caso del que no hemos hablado es el de una entidad que es una clase abstracta. La única diferencia entre una entidad que es una clase abstracta y una que es una clase concreta es la regla de Java que prohíbe que las clases abstractas se instancien. Todavía pueden definir estado persistente y comportamiento que será heredado por las subclases de entidad concretas debajo de ellas. Se pueden consultar y el resultado estará compuesto por instancias de subclases de entidad concretas. También pueden llevar los metadatos de asignación de herencia para la jerarquía.
La jerarquía de clases tenía una clase Empleado que era una clase concreta. No querríamos que los usuarios instanciaran accidentalmente esta clase y luego intentaran persistir un empleado parcialmente definido. Podríamos protegernos contra esto definiéndola como abstracta. Luego tendríamos todas nuestras clases no hoja como abstractas y las clases hoja siendo persistentes.
2. Modelos de Herencia
Jakarta Persistence proporciona soporte para tres representaciones de datos diferentes. El uso de dos de ellas es bastante común, mientras que la tercera es menos común y no es necesario admitirla, aunque está completamente definida con la intención de que los proveedores puedan verse obligados a admitirla en el futuro:
Una tabla por jerarquía de clases (single-table): una sola tabla que contiene todas las entidades en la jerarquía:
En esta estrategia, se crean tablas separadas para la superclase y para cada subclase de entidad concreta, como Ordenador y TelefonoMovil. Cada tabla contiene solo las columnas que son específicas de la subclase, además de las columnas que son heredadas de la superclase.
Sin embargo, los objetos definidos en la superclase no se incluyen en la tabla de la subclase. En su lugar, se crea una relación entre la tabla de la superclase y las tablas de las subclases. Esta relación se establece mediante una clave primaria y una clave foránea.
La clave primaria de la superclase constituye la clase foránea de las subclases. En el ejemplo anterior, la clave primaria de la tabla Dispositivo se convierte en la clave foránea de las tablas Ordenador y TelefonoMovil.
Una tabla por clase de entidad concreta (table-per-concrete-class): una tabla por cada clase de entidad concreta y sus superclases.
Este métdodo es muy similar a MappedSuperclass, pero en este caso, la clase abstracta se convierte en una entidad. Esto significa que la tabla Dispositivo también será creada en la base de datos. Esta estructura de herencia permite crear relaciones con consultas polimórficas y subclases. Sin embargo, cuando se realizan consultas, se escanean todas las tablas de las subclases, lo que puede afectar el rendimiento.
Este método debería ser evitado en los proyectos, ya que puede afectar el rendimiento de la aplicación.
Cuando existe una jerarquía de entidades, siempre tiene su origen en una clase de entidad. Recuerde que las superclases mapeadas no cuentan como niveles en la jerarquía porque solo contribuyen a las entidades debajo de ellas. La clase de entidad raíz debe significar la jerarquía de herencia al estar anotada con la anotación @Inheritance. Esta anotación indica la estrategia que se debe utilizar para el mapeo y debe ser una de las tres estrategias descritas en las siguientes secciones.
Cada entidad en la jerarquía debe definir o heredar su identificador, lo que significa que el identificador debe estar definido ya sea en la entidad raíz o en una superclase mapeada por encima de ella. Una superclase mapeada puede estar más arriba en la jerarquía de clases que donde se define el identificador.
2.1. Estrategia de una Tabla (Single-Table)
La forma más común y eficiente de almacenar el estado de múltiples clases es definir una sola tabla que contenga un conjunto de todas las posibles representaciones de estado en cualquiera de las clases de entidad. Este enfoque se llama estrategia de una tabla (single-table). Tiene la consecuencia de que, para cualquier fila de tabla que represente una instancia de una clase concreta, puede haber columnas que no tengan valores porque se aplican solo a una clase hermana en la jerarquía.
En el esquema vemos que el id se encuentra en la clase de entidad raíz Empleado y es compartido por el resto de las clases de persistencia. Todas las entidades persistentes en un árbol de herencia deben usar el mismo tipo de identificador. No necesitamos pensar mucho en ello antes de ver por qué esto tiene sentido en ambos niveles. En la capa de objetos, no sería posible realizar una operación de búsqueda polimórfica en una superclase si no hubiera un tipo de identificador común que pudiéramos pasar. De manera similar, en la tabla, necesitaríamos varias columnas de clave primaria, pero sin poder completarlas todas en cualquier inserción dada de una instancia que solo hiciera uso de una de ellas.
La tabla debe contener suficientes columnas para almacenar todo el estado en todas las clases. Una fila individual almacena el estado de una instancia de una entidad de tipo entidad concreta, lo que normalmente implicaría que algunas columnas quedarían sin completar en cada fila. Por supuesto, esto lleva a la conclusión de que las columnas mapeadas al estado de la subclase concreta deben ser nulas, lo cual normalmente no es un gran problema pero podría ser un problema para algunos administradores de bases de datos.
En general, el enfoque de una sola tabla tiende a desperdiciar más espacio en la tabla de la base de datos, pero ofrece un rendimiento máximo tanto para consultas polimórficas como para operaciones de escritura. El SQL necesario para realizar estas operaciones es simple, está optimizado y no requiere uniones.
Para especificar la estrategia de una sola tabla para la jerarquía de herencia, la clase de entidad raíz se anota con la anotación @Inheritance con su estrategia configurada en SINGLE_TABLE. En nuestro modelo anterior, esto significaría anotar la clase Empleado de la siguiente manera:
Resulta que la estrategia de una sola tabla es la predeterminada, por lo que ni siquiera necesitaríamos incluir el elemento de estrategia. Una anotación @Inheritance vacía haría el mismo efecto.
En el esquema vemos la representación de una sola tabla de nuestro modelo de jerarquía Empleado. En términos de estructura de tabla y arquitectura de esquema para la estrategia de una sola tabla, no importa si EmpleadoCompania es una superclase mapeada o una entidad.
Columna discriminante
Se ha creado una columna adicional llamada Empleado_TYPE que no se asignó a ningún campo en ninguna de las clases. Este campo tiene un propósito especial y es necesario al utilizar una sola tabla para modelar la herencia. Se llama columna discriminadora y se asigna mediante la anotación @DiscriminatorColumn en conjunción con la anotación @Inheritance que ya hemos aprendido.
El elemento name especifica el nombre de la columna que se debe usar como columna discriminadora, y si no se especifica, se utilizará por defecto una columna llamada DTYPE.
Un elemento discriminatorType dicta el tipo de la columna discriminadora. Algunas aplicaciones prefieren usar cadenas para discriminar entre los tipos de entidad, mientras que a otras les gusta usar valores enteros para indicar la clase.
El tipo de la columna discriminadora puede ser uno de los tres tipos de columna discriminadora predefinidos: INTEGER, STRING o CHAR. Si no se especifica el elemento discriminatorType, entonces se asumirá el tipo predeterminado STRING.
Valor discriminante
Cada fila en la tabla tendrá un valor en la columna discriminadora llamado valor discriminador, o un indicador de clase, para indicar el tipo de entidad que se almacena en esa fila. Por lo tanto, cada entidad concreta en la jerarquía de herencia necesita un valor discriminador específico para ese tipo de entidad para que el proveedor pueda procesar o asignar el tipo de entidad correcto cuando carga y almacena la fila.
La forma de hacer esto es mediante la anotación @DiscriminatorValue en cada clase de entidad concreta. El valor de cadena en la anotación especifica el valor discriminador que se asignará a las instancias de la clase cuando se inserten en la base de datos. Esto permitirá que el proveedor reconozca las instancias de la clase cuando emite consultas. Este valor debe ser del mismo tipo que se especificó o se predeterminó como el elemento discriminatorType en la anotación @DiscriminatorColumn.
Si no se especifica la anotación @DiscriminatorValue, entonces el proveedor utilizará una forma específica del proveedor para obtener el valor:
Si discriminatorType era STRING, entonces el proveedor simplemente usará el nombre de la entidad como la cadena indicadora de la clase.
Si discriminatorType es INTEGER, entonces tendríamos que especificar los valores discriminadores para cada clase de entidad o ninguno de ellos. Si especificáramos algunos pero no otros, no podríamos garantizar que un valor generado por el proveedor no se superponga con uno que hayamos especificado.
El Listado muestra cómo se asigna nuestra jerarquía de Empleado a una estrategia de una sola tabla.
La clase Empleado es la clase raíz, por lo que establece la estrategia de herencia y la columna discriminadora. Hemos asumido la estrategia predeterminada de SINGLE_TABLE y el tipo de discriminador STRING.
Ni las clases Empleado ni EmpleadoCompania tienen valores discriminadores, porque los valores discriminadores no deben especificarse para clases de entidad abstractas, superclases mapeadas, clases transitorias o cualquier clase abstracta en ese sentido. Solo las clases de entidad concretas usan valores discriminadores, ya que son las únicas que realmente se almacenan y recuperan de la base de datos.
La entidad EmpleadoContratado no utiliza una anotación @DiscriminatorValue, porque la cadena predeterminada “EmpleadoContratado”, que es el nombre de entidad predeterminado que se le da a la clase, es justo lo que queremos. La clase EmpleadoTiempoCompleto lista explícitamente su valor discriminador como “FTEmp”, para que sea lo que se almacene en cada fila para las instancias de EmpleadoTiempoCompleto. Mientras tanto, la clase EmpleadoTiempoParcial obtendrá “PTEmp” como su valor discriminador porque estableció su nombre de entidad en “PTEmp”, y el nombre de entidad se utiliza como el valor discriminador cuando no se especifica ninguno.
Podemos ver una muestra de algunos de los datos que podríamos encontrar dadas la configuración y el modelo anteriores. Podemos ver desde la columna discriminadora EMP_TYPE que hay tres tipos diferentes de entidades concretas. También vemos valores nulos en las columnas que no se aplican a una instancia de entidad.
2.2 Estrategia de Herencia Unida (Joined Strategy)
Desde la perspectiva de un desarrollador de Java, un modelo de datos que mapea cada entidad a su propia tabla tiene mucho sentido. Cada entidad, ya sea abstracta o concreta, tendrá su estado mapeado a una tabla diferente. Consistente con nuestra descripción anterior, las superclases mapeadas no se mapean a sus propias tablas, sino que se mapean como parte de sus subclases de entidad.
Mapear una tabla por entidad proporciona la reutilización de datos que ofrece un esquema de datos normalizado y es la forma más eficiente de almacenar datos compartidos por múltiples subclases en una jerarquía. El problema es que, cuando llega el momento de volver a ensamblar una instancia de cualquiera de las subclases, las tablas de las subclases deben unirse con las tablas de la superclase. Esto hace bastante obvio por qué esta estrategia se llama estrategia unida. También es algo más costoso insertar una instancia de entidad, porque se debe insertar una fila en cada una de sus tablas de superclase en el camino.
Recuerde que, según la estrategia de una sola tabla, el identificador debe ser del mismo tipo para cada clase en la jerarquía. En un enfoque unido, tendremos el mismo tipo de clave primaria en cada una de las tablas, y la clave primaria de una tabla de subclase también actúa como una clave externa que se une a su tabla de superclase. Esto debería sonar familiar debido a su similitud con el caso de múltiples tablas anterior en el capítulo, donde uníamos las tablas usando las claves primarias de las tablas y usábamos la anotación @PrimaryKeyJoinColumn para indicarlo. Usamos esta misma anotación en el caso de herencia unida, ya que tenemos múltiples tablas que cada una contiene el mismo tipo de clave primaria y cada una potencialmente tiene una fila que contribuye al estado final combinado de la entidad.
Aunque la herencia unida es intuitiva y eficiente en términos de almacenamiento de datos, las uniones que requiere la hacen algo costosa de usar cuando las jerarquías son profundas o extensas. Cuanto más profunda sea la jerarquía, más uniones se requerirán para ensamblar instancias de la entidad concreta en la parte inferior. Cuanto más amplia sea la jerarquía, más uniones se requerirán para hacer consultas en una superclase de entidad.
En la gráfica vemos nuestro ejemplo de Empleado mapeado a una arquitectura de tabla unida. Los datos de una subclase de entidad se distribuyen en las tablas de la misma manera que se distribuyen en la jerarquía de clases. Cuando se utiliza una arquitectura unida, la decisión de si EmpleadoCompania es una superclase mapeada o una entidad marca la diferencia, ya que las superclases mapeadas no se asignan a tablas. Una entidad, incluso si es una clase abstracta, siempre lo hace. La gráfica lo muestra como una superclase mapeada, pero si fuera una entidad, entonces existiría una tabla adicional COMPANY_EMP con columnas ID y VACATION, y la columna VACATION en las tablas FT_EMP y PT_EMP no estaría presente.
Para mapear una jerarquía de entidades a un modelo unido, la anotación @Inheritance solo necesita especificar JOINED como la estrategia. Al igual que en el ejemplo de una sola tabla, las subclases adoptarán la misma estrategia que se especifica en la superclase de entidad raíz.
Aunque hay múltiples tablas para modelar la jerarquía, la columna discriminadora solo se define en la tabla raíz, por lo que la anotación @DiscriminatorColumn se coloca en la misma clase que la anotación @Inheritance.
Consejo
Algunos proveedores ofrecen implementaciones de herencia unida sin el uso de una columna discriminadora. Se deben usar columnas discriminadoras si se requiere portabilidad del proveedor.
Nuestro ejemplo de jerarquía Empleado se puede mapear utilizando el enfoque unido que se muestra en el Listado. En este ejemplo, utilizamos columnas discriminadoras de tipo entero en lugar del tipo de cadena predeterminado.
Jerarquía de Entidades Mapeada Usando la Estrategia Unida
2.3. Estrategia de una Tabla por Clase Concreta (Table-per-Concrete-Class Strategy)
Un tercer enfoque para mapear una jerarquía de entidades es utilizar una estrategia donde se define una tabla por clase concreta. Esta arquitectura de datos va en la dirección opuesta a la no normalización de los datos de entidad y asigna cada clase de entidad concreta y todo su estado heredado a una tabla separada. Esto tiene el efecto de hacer que todo el estado compartido se redefina en las tablas de todas las entidades concretas que lo heredan. Este enfoque no es necesario ser admitido por los proveedores, pero se incluye porque se anticipa que será necesario en una versión futura de la API. Lo describimos brevemente por completitud.
El aspecto negativo de usar esta estrategia es que hace que las consultas polimórficas a lo largo de una jerarquía de clases sean más costosas que las otras estrategias. El problema es que debe emitir varias consultas separadas a través de cada una de las tablas de subclases o consultar todas ellas usando una operación UNION, que generalmente se considera costosa cuando hay mucha cantidad de datos. Si hay clases concretas que no son hojas, entonces cada una de ellas tendrá su propia tabla. Las subclases de las clases concretas tendrán que almacenar los campos heredados en sus propias tablas, junto con sus propios campos definidos
.
El lado positivo de las jerarquías de tablas por clase concreta, en comparación con las jerarquías unidas, se ve en casos de consulta sobre instancias de una sola entidad concreta. En el caso unido, cada consulta requiere una unión, incluso cuando se consulta a través de una sola clase de entidad concreta. En el caso de tabla por clase concreta, es similar a la jerarquía de una sola tabla porque la consulta se limita a una sola tabla. Otra ventaja es que desaparece la columna discriminadora. Cada entidad concreta tiene su propia tabla separada, y no hay mezcla ni compartición de esquema, por lo que nunca se necesita un indicador de clase.
Mapear nuestro ejemplo a este tipo de jerarquía es cuestión de especificar la estrategia como TABLE_PER_CLASS y asegurarse de que haya una tabla para cada una de las clases concretas. Si se está utilizando una base de datos heredada, entonces las columnas heredadas podrían tener nombres diferentes en cada una de las tablas concretas, y la anotación @AttributeOverride sería útil. En este caso, la tabla CONTRACT_EMP no tenía las columnas NAME y fechaInicio, sino que en su lugar tenía FULLNAME y SDATE para los campos name y fechaInicio definidos en Empleado. Si el atributo que queríamos anular fuera una asociación en lugar de un mapeo de estado simple, aún podríamos anular el mapeo, pero necesitaríamos usar la anotación @AssociationOverride en lugar de @AttributeOverride. La anotación @AssociationOverride nos permite anular las columnas de unión utilizadas para hacer referencia a la entidad objetivo de una asociación de muchos a uno o de uno a uno definida en una superclase mapeada. Para mostrar esto, necesitamos agregar un atributo manager a la superclase mapeada EmpleadoCompania. La columna de unión se asigna por defecto en la clase EmpleadoCompania a la columna MANAGER en las dos tablas de subclases FT_EMP y PT_EMP, pero en PT_EMP el nombre de la columna de unión es en realidad MGR. Anulamos la columna de unión agregando la anotación @AssociationOverride a la clase EmpleadoTiempoParcial y especificando el nombre del atributo que estamos anulando y la columna de unión que estamos anulando. El Listado muestra un ejemplo completo de los mapeos de entidades, incluidas las anulaciones.
Jerarquía de Entidades Mapeada Usando una Estrategia de Tabla por Clase Concreta
La organización de la tabla muestra cómo se asignan estas columnas a las tablas concretas. Consulta el esquema para obtener una imagen clara de cómo se verían las tablas y cómo se almacenarían los diferentes tipos de instancias de empleados.
3. Herencia Mixta
Debemos comenzar esta sección diciendo que la práctica de mezclar tipos de herencia dentro de una sola jerarquía de herencia actualmente está fuera de la especificación. Lo estamos incluyendo porque es útil e interesante, pero ofrecemos una advertencia de que podría no ser portátil depender de dicho comportamiento, incluso si su proveedor lo admite.
Además, realmente tiene sentido mezclar solo tipos de herencia de una sola tabla y herencia unida. Mostramos un ejemplo de mezcla de estos dos, teniendo en cuenta que el soporte para ellos depende del proveedor. La intención es que, en futuras versiones de la especificación, los casos más útiles se estandaricen y se requiera que las implementaciones compatibles los admitan.
La premisa para mezclar tipos de herencia es que es muy posible que un modelo de datos incluya una combinación de diseños de una sola tabla y tablas unidas dentro de una sola jerarquía de entidades. Esto se puede ilustrar tomando nuestro ejemplo unido en el esquema y almacenando las instancias de EmpleadoTiempoCompleto y EmpleadoTiempoParcial en una sola tabla. Esto produciría un modelo como el que se muestra.
En este ejemplo, se utiliza la estrategia unida para las clases Empleado y EmpleadoContratado, mientras que las clases EmpleadoCompania, EmpleadoTiempoCompleto y EmpleadoTiempoParcial vuelven a un modelo de una sola tabla. Para hacer este cambio de estrategia de herencia en el nivel de EmpleadoCompania, necesitamos realizar un cambio simple en la jerarquía. Necesitamos convertir EmpleadoCompania en una entidad abstracta en lugar de una superclase mapeada para que pueda llevar los nuevos metadatos de herencia. Tenga en cuenta que esto es simplemente un cambio de anotación, sin realizar ningún cambio en el modelo de dominio.
Las estrategias de herencia se pueden mapear como se muestra en el Listado. Observe que no necesitamos tener una columna discriminadora para la subjerarquía de una sola tabla, ya que ya tenemos una en la tabla superior EMP.
Listado. Jerarquía de Entidades Mapeada Usando Estrategias Mixtas
Cancion. Clase que representa una canción, con: idCancion (Long), titulo (String), autor (String), duración (int), dataPublicacion (LocalDate).
MediaSong. Hereda de Cancion y, a mayores, tiene el audio, guardado como byte[].
PlayList. Contiene la lista de Reproducibles, en este caso, de tipo MediaSong, así como idPlayList (Long), nome (String) y dataCreacion (LocalDate).
Reproducible, IPlayList, PlayListObserver: interfaces que deben implantar, aquellas clases que sean reproducibles, una PlayList o un observador de PlayList respectivamente (no las precisáis).
Crea las relaciones necesarias entre las clases para ajustarse a los parámetros de la base de datos.
Especificación JPA
1. Herencia
Una entidad puede heredar de otra clase de entidad.
Las entidades admiten herencia,asociaciones polimórficas y consultas polimórficas.
Tanto las clases abstractas como las clases concretas pueden ser entidades. Ambas pueden llevar la anotación @Entity, mapearse como entidades y consultarse como entidades.
Las entidades pueden extender clases que no son entidades y viceversa.
1.1. Clases de Entidad Abstractas
Se puede especificar una clase abstracta como entidad. Una entidad abstracta difiere de una entidad concreta solo en que no se puede instanciar directamente. Se mapea como una entidad y puede ser el objetivo de consultas (que operarán y/o recuperarán instancias de sus subclases concretas).
Una clase de entidad abstracta se anota con la anotación @Entity (o se designa en el descriptor XML como una entidad, que no veremos).
El siguiente ejemplo muestra el uso de una clase de entidad abstracta en la jerarquía de herencia de entidades.
Ejemplo: Clase abstracta como entidad
Ojo con las mayúsculas y minúsculas en los nombres de las tablas. Por defecto, el nombre de la tabla es el nombre de la clase en mayúsculas, pero esto depende del proveedor JDBC y si usamos H2, si hemos puesto la propiedad DATABASE_TO_UPPER=FALSE o hibernate.implicit_naming_strategy en true o false.
@Entity@Table(name="Empleado")
@Inheritance(strategy=JOINED) // Por defecto es SINGLE_TABLE. JOINED es la estrategia de subclases unidaspublicabstractclassEmpleado {
@Idprotected Integer idEmpleado;
@Versionprotected Integer version;
@ManyToOneprotected Direccion direccion;
// ...}
@Entity@Table(name="EmpleadoTC") // Por defecto es EmpleadoTiempoCompleto (en mayúsculas, depende del proveedor JDBC)@DiscriminatorValue("TC")
@PrimaryKeyJoinColumn(name="IdEmpleadoTC") // Por defecto es idEmpleadopublicclassEmpleadoTiempoCompletoextends Empleado {
// Hereda idEmpleado, pero se mapea en esta clase como EmpleadoTC.IdEmpleadoTC// Hereda version mapeada a Empleado.version// Hereda dirección mapeada a Empleado.direccion fk// Por defecto a EmpleadoTC.salarioprotected Integer salario;
// ...}
@Entity@Table(name="EmpleadoTP") // Por defecto es EmpleadoTiempoParcial (en mayuscúlas, depende del proveedor JDBC)@DiscriminatorValue("TP")
// La columna PK es EmpleadoTP.idEmpleado debido a la clave foránea predeterminada heredada de EmpleadopublicclassEmpleadoTiempoParcialextends Empleado {
protected Float salarioPorHora;
// ...}
1.2. Superclases Mapeadas
Una entidad puede heredar de una superclase que proporciona estado de entidad persistente e información de mapeo, pero que no es en sí misma una entidad.
Por lo general, el propósito de dicha superclase mapeada es definir información de estado y mapeo que es común a múltiples clases de entidades.
Una superclase mapeada, a diferencia de una entidad, no es consultable y no debe pasarse como argumento a las operaciones de EntityManager o Query. Las relaciones persistentes definidas por una superclase mapeada deben ser unidireccionales.
Se pueden especificar tanto clases abstractas como clases concretas como superclases mapeadas. Se utiliza la anotación MappedSuperclass (o el elemento de descriptor XML mapped-superclass) para designar una superclase mapeada.
Una clase designada como superclase mapeada no tiene una tabla separada definida para ella. Su información de mapeo se aplica a las entidades que heredan de ella.
Una clase designada como superclase mapeada se puede mapear de la misma manera que una entidad, excepto que los mapeos se aplicarán solo a sus subclases, ya que no existe una tabla para la superclase mapeada en sí. Cuando se aplican a las subclases, los mapeos heredados se aplicarán en el contexto de las tablas de las subclases. La información de mapeo se puede anular en dichas subclases utilizando las anotaciones AttributeOverride y AssociationOverride o los elementos XML correspondientes.
Todos los demás valores predeterminados de mapeo de entidad se aplican igualmente a una clase designada como superclase mapeada.
El siguiente ejemplo ilustra la definición de una clase concreta como una superclase mapeada.
Ejemplo: Clase concreta como superclase mapeada
@MappedSuperclasspublicclassEmpleado {
@Idprotected Integer idEmpleado;
@Versionprotected Integer version;
@ManyToOne@JoinColumn(name="ADDR")
protected Direccion direccion;
public Integer getIdEmpleado() { ... }
publicvoidsetIdEmpleado(Integer id) { ... }
public Direccion getDireccion() { ... }
publicvoidsetDireccion(Direccion addr) { ... }
}
// La tabla predeterminada es la tabla EmpleadoTiempoCompleto@EntitypublicclassEmpleadoTiempoCompletoextends Empleado {
// Campo idEmpleado heredado mapeado a EmpleadoTiempoCompleto.IDEMPLEADO// Campo versión heredado mapeado a EmpleadoTiempoCompleto.VERSION// Campo dirección heredado mapeado a EmpleadoTiempoCompleto.ADDR fk
// Por defecto a EmpleadoTiempoCompleto.SALARIOprotected Integer salario;
publicEmpleadoTiempoCompleto() {}
public Integer getSalario() { ... }
publicvoidsetSalario(Integer salario) { ... }
}
@Entity@Table(name="PT_EMP")
@AssociationOverride(name="direccion", joincolumns=@JoinColumn(name="ADDR_ID"))
publicclassEmpleadoTiempoParcialextends Empleado {
// Campo idEmpleado heredado mapeado a PT_EMP.IDEMPLEADO// Campo versión heredado mapeado a PT_EMP.VERSION// Mapeo de campo dirección anulado a PT_EMP.ADDR_ID fk@Column(name="SALARIO_POR_HORA")
protected Float salarioPorHora;
publicEmpleadoTiempoParcial() {}
public Float getSalarioPorHora() { ... }
publicvoidsetSalarioPorHora(Float salario) { ... }
}
1.3. Clases no Entidad en la Jerarquía de Herencia de Entidades
Una entidad puede tener una superclase no entidad, que puede ser una clase concreta o abstracta.
La superclase no entidad sirve únicamente para la herencia de comportamiento. El estado de una superclase no entidad no es persistente. Cualquier estado heredado de superclases no entidad no es persistente en una clase de entidad heredera. Este estado no persistente no es gestionado por el administrador de entidades. Se ignoran cualquier anotación en tales superclases.
Las clases no entidad no se pueden pasar como argumentos a los métodos de las interfaces EntityManager o Query y no pueden llevar información de mapeo.
El siguiente ejemplo ilustra el uso de una clase no entidad como superclase de una entidad.
El mapeo de jerarquías de clases se especifica a través de metadatos.
Hay tres estrategias básicas que se utilizan al mapear una clase o jerarquía de clases a una base de datos relacional:
Una tabla única por jerarquía de clases.
Una estrategia de subclases unidas, en la que los campos específicos de una subclase se asignan a una tabla separada que los campos comunes de la clase principal, y se realiza una unión para instanciar la subclase.
Una tabla por clase de entidad concreta.
Se requiere que una implementación admita la estrategia de una tabla única por jerarquía de clases y la estrategia de subclases unidas.
La compatibilidad con la estrategia de tabla por clase de entidad concreta es opcional en esta versión. Las aplicaciones que utilicen esta estrategia de mapeo no serán portátiles.
No se requiere soporte para la combinación de estrategias de herencia dentro de una sola jerarquía de herencia de entidades según esta especificación.
2.1. Estrategia de Una Tabla por Jerarquía de Clases
En esta estrategia, todas las clases de una jerarquía se asignan a una sola tabla. La tabla tiene una columna que sirve como “columna de discriminador”, es decir, una columna cuyo valor identifica la subclase específica a la que pertenece la instancia representada por la fila.
Esta estrategia de mapeo proporciona un buen soporte para relaciones polimórficas entre entidades y para consultas que abarcan la jerarquía de clases.
Sin embargo, tiene la desventaja de que requiere que las columnas que corresponden al estado específico de las subclases sean nulas.
2.2. Estrategia de Subclases Unidas
En la estrategia de subclases unidas, la raíz de la jerarquía de clases se representa mediante una sola tabla. Cada subclase se representa mediante una tabla separada que contiene aquellos campos que son específicos de la subclase (no heredados de su superclase), así como la(s) columna(s) que representan su clave primaria. La(s) columna(s) de la clave primaria de la tabla de la subclase sirve como clave foránea a la clave primaria de la tabla de la superclase.
Esta estrategia proporciona soporte para relaciones polimórficas entre entidades.
Tiene la desventaja de que requiere que se realice una o más operaciones de unión para instanciar instancias de una subclase. En jerarquías de clases profundas, esto puede conducir a un rendimiento inaceptable. Las consultas que abarcan la jerarquía de clases también requieren uniones.
2.3. Estrategia de Una Tabla por Clase de Entidad Concreta
En esta estrategia de mapeo, cada clase se asigna a una tabla separada. Todas las propiedades de la clase, incluidas las propiedades heredadas, se asignan a columnas de la tabla de la clase.
Esta estrategia tiene las siguientes desventajas:
Proporciona un soporte deficiente para relaciones polimórficas.
Por lo general, requiere que se emitan consultas UNION SQL (o una consulta SQL separada por subclase) para consultas que están destinadas a abarcar la jerarquía de clases.
Para crear un proyecto con JPA y Hibernate, se puede utilizar el asistente de creación de proyectos de Eclipse o IntelliJ IDEA; sin embargo, con la versión Community de IntelliJ IDEA no se puede crear un proyecto con JPA a través del asistente.
Crea un proyecto Java Maven y añade las dependencias de Hibernate y la API de Jakarta Persistence.
Ejercicio 01.02. Creación de un archivo de configuración de persistencia
Crea un directorio META-INF en el directorio src/main/resources y añade un archivo persistence.xml con la configuración de la unidad de persistencia con el nombre com.sanclemente.ad.jpa.exemplo.
El fichero de configuración persistence.xmldebe apuntar a una base de datos H2 en memoria. Además, debes añadir los Drivers de H2 para que la aplicación pueda conectarse a la base de datos:
Ten en cuenta que precisas crear la base de datos en memoria H2 y añadir las tablas necesarias, por lo que el parámetro jakarta.persistence.schema-generation.database.action debe ser “create”.
Ejercicio 01.03. Creación de una entidad
Crea una entidad Estudiante con idEstudiante (Long), nombre, apellidos, fechaDeNacimiento y dirección. Añade los atributos necesarios y las anotaciones para que sea una entidad. La clave primaria será idEstudiante de tipo autoincremental.
Ejercicio 01.04. Creación de una entidad
Crea una clase AppEstudiante que se conecte a la base de datos y añada un estudiante a la tabla de la base de datos.
Aunque lo veremos más adelante, lo que precisamos es crear un gestor de entidades e invocar al método persist para añadir un estudiante a la base de datos:
publicclassAppEstudiante {
publicstaticvoidmain(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("com.sanclemente.ad.jpa.exemplo");
EntityManager em = emf.createEntityManager();
Estudiante estudiante =new Estudiante("Juan", "Pérez", LocalDate.of(2000, 1, 1), "Calle Mayor, 1");
em.getTransaction().begin();
em.persist(estudiante);
em.getTransaction().commit();
// IMprime el estudiante para ver si se ha añadido correctamente y tiene un id em.close();
emf.close();
}
}
Para recuperarlo precisamos invocar al método find del gestor de entidades:
Estudiante estudiante = em.find(Estudiante.class, 1L); // Recupera el estudiante con id 1
Ejercicio 03.01. Creación de una aplicación de persistencia de una biblioteca
Queremos desarrollar una aplicación para una biblioteca y necesitamos interactuar con una base de datos que contiene información sobre los libros que tenemos en nuestra colección.
Para ello, vamos a crear una clase Book que represente la entidad libro, la clase Contido y otra clase BookDAO que nos permita realizar operaciones básicas CRUD (Create, Read, Update y Delete) sobre la tabla Book en la base de datos.
Además, precisamos una clase BibliotecaJpaManager para la gestión y obtención de los objetos de tipo EntityManagerFactory de una manera eficiente. Emplearemos el patrón Singleton para el gestor BibliotecaJpaManager, que tenga un único objeto de tipo EntityManagerFactory y que nos permita obtener un objeto de tipo EntityManager para realizar las operaciones sobre la base de datos (queremos que el objeto de tipo EntityManagerFactory sea único para cada unidad de persistencia, para cada unidad de persistencia, no así el EntityManager, que podrá hacer varios para cada unidad de persistencia).
A) BASE DE DATOS (es la misma base de datos que hemos empleado en la unidad de bases de datos con JDBC):
Está formada por una tabla Book y una tabla Contido. La tabla Book tiene una estructura SIMILAR a la siguiente:
Columna
Tipo de dato
Descripción
idBook
int
Identificador único del ejemplar del libro
isbn
varchar(13)
Identificador del libro
titulo
varchar(100)
Título del libro
autor
varchar(100)
Autor del libro
anho
int
Año de publicación del libro
disponible
boolean
Indica si el libro está disponible
portada
Blob
Portada del libro en formato binario
dataPublicacion
Date
Fecha de publicación del libro
-- PUBLIC.Book definition
-- Drop table
-- DROP TABLE PUBLIC.Book;
CREATETABLEPUBLIC.Book (
idBook INTEGER NOTNULL AUTO_INCREMENT,
isbn CHARACTER VARYING(13) NOTNULL,
titulo CHARACTER VARYING(255) NOTNULL,
autor CHARACTER VARYING(255),
anho INTEGER,
disponible BOOLEAN DEFAULTTRUE,
portada BINARY LARGEOBJECT,
dataPublicacion DATE,
CONSTRAINT BOOK_PK PRIMARYKEY (idBook)
);
CREATEUNIQUEINDEX IdBookPK ONPUBLIC.Book (idBook);
CREATEINDEX IdxBookISBN ONPUBLIC.Book (isbn);
CREATEINDEX IdxBookTitle ONPUBLIC.Book (titulo);
CREATEUNIQUEINDEX PRIMARY_KEY_93 ONPUBLIC.Book (idBook);
La tabla Contido tiene una estructura SIMILAR a la siguiente:
Columna
Tipo de dato
Descripción
idContido
int
Identificador único del contenido del libro
idBook
int
Identificador del libro
contido
Blob
Contenido del libro en formato binario
*idBook es una clave foránea+ que referencia a la tabla Book.
El fichero persistencia.xml debe tener la siguiente configuración:
<?xml version="1.0" encoding="UTF-8"?><persistencexmlns="https://jakarta.ee/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"version="3.0"><persistence-unitname="bibliotecaH2"transaction-type="RESOURCE_LOCAL"><provider>org.hibernate.jpa.HibernatePersistenceProvider</provider><!-- <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>--><exclude-unlisted-classes>false</exclude-unlisted-classes><--falsesinoselistanlasclasesenelarchivodeconfiguración--><properties><!-- <property name="jakarta.persistence.jdbc.url" value="jdbc:mariadb://localhost:3306/peliculas"/>--><propertyname="jakarta.persistence.jdbc.url"value="jdbc:h2:rutaALaBaseDeDatos;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO"/><!-- Ejemplo con Access --><!--<property name="jakarta.persistence.jdbc.url" value="jdbc:ucanaccess://rutabase_base_datos.mdb"/>--><!-- <property name="jakarta.persistence.jdbc.user" value="root"/>--><!-- <property name="jakarta.persistence.jdbc.password" value=""/>--><propertyname="jakarta.persistence.jdbc.user"value=""/><propertyname="jakarta.persistence.jdbc.password"value=""/><!-- <property name="jakarta.persistence.jdbc.driver" value="net.ucanaccess.jdbc.UcanaccessDriver"/>--><propertyname="jakarta.persistence.jdbc.driver"value="org.h2.Driver"/><!-- Automáticamente, genera el esquema de la base de datos --><propertyname="jakarta.persistence.schema-generation.database.action"value="none"/><!-- Muestra por pantalla las sentencias SQL --><propertyname="hibernate.show_sql"value="true"/><propertyname="hibernate.format_sql"value="true"/><propertyname="hibernate.highlight_sql"value="true"/><!-- <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />--><!-- para HSQLDB y Ucanaccess --><propertyname="hibernate.dialect"value="org.hibernate.dialect.H2Dialect"/></properties></persistence-unit></persistence>
B) Clase BibliotecaJpaManager:
Mediante el patrón Singleton crea una clase BibliotecaJpaManager, mediante el patrón Singleton de manera que tenga un atributo emFactory de tipo EntityManagerFactory y que nos permita obtener un objeto de tipo EntityManager para realizar las operaciones sobre la base de datos.
Además, debe tener un método estático getEntityManager que devuelva un objeto de tipo EntityManager y que se encargue de crear el objeto EntityManager.
Hazlo con Thread-Safe y doble comprobación.
Reto: haz que la clase BibliotecaJpaManager tenga un singleton para cada factory, guardándolos en un mapa con el nombre de la unidad de persistencia como clave:
La lista de Contido es una lista de objetos de tipo Contido que representan los contenidos del libro. La clase Contido tiene los siguientes atributos: idContido y contido.
Ten en cuenta que existe en la base de datos una tabla Contido con los campos idContido y contido y una referencia al libro mediante una clave foránea idBook. De momento, no incluyas la List de contenidos en la clase Book, hazlos transient (bien con la anotación @Transient o con la palabra reservada transient), hasta que veamos las relaciones, que será @OneToMany.
Los métodos “set” de las propiedades deben devolver una referencia al propio objeto para poder encadenarlos.
IMPORTANTE: ten en cuenta que los atributos de la clase Book no coinciden con los campos de tabla por lo que debes refactorizar: author -> autor, ano -> anho, avaliable -> disponible, … o emplear la anotación @Column para mapear los atributos de la clase con los campos de la tabla.
Métodos de la clase Book (ya implantados):
Get y set para cada atributo.
setPortada (sin implantar): recibe File y lo asigna al atributo portada.
setPortada (sin implantar): recibe un array de bytes y lo asigna al atributo portada.
setPortada (Sin implantar): recibe un String con el nombre del fichero y lo asigna al atributo portada.
getImage: devuelve un objeto de tipo Image con la portada del libro.
public Image getImage() {
if (portada !=null) {
try (ByteArrayInputStream bis =new ByteArrayInputStream(portada)) {
return ImageIO.read(bis);
} catch (IOException e) {
}
}
returnnull;
}
equals y hashCode: considerando que son iguales cuando tienen el mismo isbn.
Además, el método hashCode debe devolver un valor coherente con el método equals (todos los objetos iguales deben tener, al menos el mismo hashCode).
toString: devuelve el título, el autor y el año. Si no está disponible escribe un asterisco.
D) Clase Contido implementa Serializable:
A diferencia de la clase empleada en la unidad de bases de datos con JDBC, la clase Contido no debe tener referencia al idBook, pues no es la mejor práctica (está hecho sólo a modo de ejemplo), debe tener, si queremos la relación bidireccional, una referencia a Book.
idContido: Long (autonumérico)
contido: String (contenido del libro en formato texto). Puedes hacer un atributo de tipo String o byte[] (para almacenar el contenido en formato binario), en cualquier caso, deberías modificar la tabla Contido en la base de datos.
Book book (relación con la clase Book)
Si has implantado la clase ContidoDao, debes modificar los métodos que obtienen el idBook del book:
contido.getBook().getIdBook();
E) Clase BookJPADao:
Esta clase, al igual que la clase BookDao, la clase BookJPADaodebe implantar la interface Dao<T>, de modo que tenga un objeto de tipo EntityManagercomo atributo. En sistemas empresariales, como la gestión de transacciones no se suele hacer por método, se guarda una referencia a la clase EntityManagerFactory y se gestiona por medio de try-with-resources para manejar los cierres de los EntityManager.
Dao<T>:
import java.util.List;
/**
*
* @author pepecalo
* @param <T> Tipo de dato del objeto
*/publicinterfaceDAO<T> {
T get(long id);
List<T>getAll();
voidsave(T t);
voidupdate(T t);
voiddelete(T t);
publicbooleandeleteById(long id);
public List<Integer>getAllIds();
publicvoidupdateLOB(T book, String f); // en BookJPADao recibe un objeto de tipo Book y un String con el nombre del ficheropublicvoidupdateLOBById(long id, String f);
voiddeleteAll();
}
Clase BookJPADao:
Implementa la interfaz DAO<Book> y gestiona las operaciones CRUD sobre la tabla Book de la base de datos.
Tiene como atributo un objeto de tipo EntityManager que recoge en el constructor.
Clase BookDAOFactory:
Factory de clases que implanten la interfaz DAO<Book>.
Implementa un método estático getBookDAO que recoge el tipo de DAO que se va a emplear y devuelve el objeto de tipo BookJPADAO. Sería interesante hacer cambios para que getBookDao recoja los parámetros necesarios como propiedades de la base de datos, nombre del archivo JSON, nombre de la unidad de persistencia, etc.
Ejecuta la aplicación para que haga uso del BookDaoFactory para obtener un objeto de tipo DAO<Book> para asignarlo al controlador de la aplicación. La aplicación debe funcionar igual que con JDBC, pero ahora con JPA.
Haz pruebas con los dos tipos de DAO. ¿Has notado alguna diferencia? Haz mejoras sobre el funcionamiento de la aplicación.
Puedes hacer pruebas de persistencia de libros en la base de datos:
Book libro =new Book("9788424937744", "Tractatus logico-philosophicus-investigaciones filosóficas", "Ludwig Wittgenstein", 2017, false);
libro =new Book("9788499088150", "Verano", "J. M. Coetzee", 2011, true);
Ejercicio 04.01. Descarga y creación de la base de datos de JokeAPI
Dado el modelo de la aplicación de JokeAPI, en la que tenemos las enumeraciones Categoriam TipoChiste, Flag y la clase Chiste, vamos a crear una base de datos con JPA y los chistes de la API.
Enumeraciones
A) La enumeración Categoria tiene los siguientes valores:
Detalle de implementación de la enumeración Categoría
package com.javhoz.ad.chistes.model;
/**
* Updated by javhoz on 16/01/2025.
* <p>
* Enumeración de categorías de chistes.
* Pueden ser: Any, Misc, Programming, Dark, Pun, Spooky, Christmas
* Atributo: nombre, de tipo cadena.
*/publicenum Categoria {
ANY("Any"),
MISC("Misc"),
PROGRAMMING("Programming"),
DARK("Dark"),
PUN("Pun"),
SPOOKY("Spooky"),
CHRISTMAS("Christmas");
privatefinal String nombre;
Categoria(String nombre) {
this.nombre= nombre;
}
public String getNombre() {
return nombre;
}
/**
* Devuelve la categoría a partir de su nombre.
*
* @param nombre Nombre de la categoría
* @return Categoría
*/publicstatic Categoria getCategoria(String nombre) {
for (Categoria c : Categoria.values()) {
if (c.getNombre().equals(nombre)) {
return c;
}
}
returnnull;
}
/**
* Sobreescribe el método toString() para que devuelva el nombre de la categoría.
*
* @return Nombre de la categoría
* @see java.lang.Enum#toString()
*/@Overridepublic String toString() {
return nombre;
}
}
B) La enumeración TipoChiste contiene los siguientes valores:
Detalle de implementación de la enumeración TipoChiste
package com.javhoz.ad.chistes.model;
/**
* Updated by javhoz on 16/01/2025.
* Enumeración de tipos de chistes.
* Pueden ser: single, twopart
* Atributo: String nombre.
* Constructor: TipoChiste(String nombre)
* @see Categoria
* @see Flag
* @see Chiste
*
*/publicenum TipoChiste {
SINGLE("single"),
TWOPART("twopart");
privatefinal String nombre;
TipoChiste(String nombre) {
this.nombre= nombre;
}
public String getNombre() {
return nombre;
}
/**
* Devuelve el tipo de chiste a partir de su nombre.
* @param nombre Nombre del tipo de chiste
* @return Tipo de chiste
*/publicstatic TipoChiste getTipoChiste(String nombre) {
for (TipoChiste tc : TipoChiste.values()) {
if (tc.getNombre().equals(nombre)) {
return tc;
}
}
returnnull;
}
/**
* Sobreescribe el método toString() para que devuelva el nombre del tipo de chiste.
* @return Nombre del tipo de chiste
* @see java.lang.Enum#toString()
*/@Overridepublic String toString() {
return nombre;
}
}
C) La enumeración Flag contiene los siguientes valores:
Flag es una enumeración con los siguientes valores:
```java
publicenum Flag {
EXPLICIT("Explicit"),
NSFW("NSFW"),
RELIGION("Religion"),
POLITICAL("Political"),
RACIST("Racist"),
SEXIST("Sexist");
//...}
Detalle de implementación de la enumeración Flag
package com.javhoz.ad.chistes.model;
/**
* Updated by javhoz on 16/01/2025.
* Enumeración de banderas de chistes.
* Pueden ser: NSFW, RELIGION, POLITICAL, RACIST, SEXIST
* Atributo: String nombre.
* Constructor: Flag(String nombre)
* @see Categoria
* @link <a href="https://v2.jokeapi.dev/flags">https://v2.jokeapi.dev/flags</a>
*/publicenum Flag {
EXPLICIT("Explicit"),
NSFW("NSFW"),
RELIGION("Religion"),
POLITICAL("Political"),
RACIST("Racist"),
SEXIST("Sexist");
privatefinal String nombre;
Flag(String nombre) {
this.nombre= nombre;
}
public String getNombre() {
return nombre;
}
/**
* Devuelve la bandera a partir de su nombre.
* @param nombre Nombre de la bandera
* @return Bandera
*/publicstatic Flag getFlag(String nombre) {
// Con expresiones lambda:return java.util.Arrays.stream(Flag.values()).filter(f -> f.getNombre().equals(nombre)).findFirst()
.orElse(null);
/* // Con un bucle for:
// for (Flag f : Flag.values()) {
// if (f.getNombre().equals(nombre)) {
// return f;
// }
// }
// return null;
*/ }
/**
* Sobreescribe el método toString() para que devuelva el nombre de la bandera.
* @return Nombre de la bandera
* @see java.lang.Enum#toString()
*/@Overridepublic String toString() {
return nombre;
}
}
D)Lenguaje es una enumeración con los siguientes valores:
package com.javhoz.ad.chistes.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Updated by javhoz on 16/01/2025.
* <p>
* Clase que representa un chiste.
* Atributos: id de tipo int, categoria de tipo Categoria, idiomade tipo Lenguaje, tipo de TipoChiste,
* List<Flag> banderas, String chiste, String respuesta.
*/publicclassChiste {
privateint id;
private Categoria categoria;
private TipoChiste tipo;
privatefinal List<Flag> banderas;
private String chiste;
private String respuesta;
private Lenguaje lenguaje;
/**
* Constructor de la clase Chiste.
* @param id Identificador del chiste
* @param categoria Categoría del chiste
* @param idioma Idioma del chiste
* @param tipo Tipo del chiste
* @param chiste Chiste
* @param respuesta Respuesta del chiste
*/publicChiste(int id, Categoria categoria, String idioma, TipoChiste tipo, String chiste, String respuesta) {
this.id= id;
this.categoria= categoria;
this.tipo= tipo;
this.chiste= chiste;
this.respuesta= respuesta;
this.banderas=new ArrayList<>();
this.lenguaje= Lenguaje.getLenguaje(idioma);
}
/**
* Constructor por defecto de la clase Chiste.
*
*/publicChiste() {
// this.id = 0;this.categoria= Categoria.ANY;
this.lenguaje= Lenguaje.EN;
this.tipo= TipoChiste.SINGLE;
this.chiste="";
this.respuesta="";
this.banderas=new ArrayList<>();
}
/**
* Devuelve el identificador del chiste.
* @return Identificador del chiste
*/publicintgetId() {
return id;
}
/**
* Establece el identificador del chiste.
* @param id Identificador del chiste
*/publicvoidsetId(int id) {
this.id= id;
}
/**
* Devuelve la categoría del chiste.
* @return Categoría del chiste
*/public Categoria getCategoria() {
return categoria;
}
public String getCategoriaString() {
return categoria.getNombre();
}
/**
* Establece la categoría del chiste.
* @param categoria Categoría del chiste
*/publicvoidsetCategoria(Categoria categoria) {
this.categoria= categoria;
}
publicvoidsetCategoria(String categoria) {
this.categoria= Categoria.getCategoria(categoria);
}
public Lenguaje getLenguaje() {
return lenguaje;
}
public String getLenguajeString() {
return lenguaje.getLenguaje();
}
publicvoidsetLenguaje(String lenguaje) {
this.lenguaje= Lenguaje.getLenguaje(lenguaje);
}
publicvoidsetLenguaje(Lenguaje lenguaje) {
this.lenguaje= lenguaje;
}
/**
* Devuelve el tipo del chiste.
* @return Tipo del chiste
*/public TipoChiste getTipo() {
return tipo;
}
public String getTipoString() {
return tipo.getNombre();
}
/**
* Establece el tipo del chiste.
* @param tipo Tipo del chiste
*/publicvoidsetTipo(TipoChiste tipo) {
this.tipo= tipo;
}
publicvoidsetTipo(String tipo) {
this.tipo= TipoChiste.getTipoChiste(tipo);
}
/**
* Devuelve las banderas del chiste.
* @return Banderas del chiste
*/public List<Flag>getBanderas() {
return banderas;
}
/**
* Añade una bandera al chiste.
* @param flag Bandera a añadir
*/publicvoidaddFlag(Flag flag) {
banderas.add(flag);
}
publicbooleanremoveFlag(Flag bandera) {
return banderas.remove(bandera);
}
/**
* Si el chiste tiene esa bandera, devuelve true.
* @param bandera Bandera a comprobar
* @return true si el chiste tiene esa bandera, false en caso contrario
*/publicbooleancontainsFlag(Flag bandera) {
return banderas.contains(bandera);
}
/**
* Devuelve el chiste como cadena de caracteres.
* @return Chiste como String
*/public String getChiste() {
return chiste;
}
/**
* Establece el chiste.
* @param chiste Chiste
*/publicvoidsetChiste(String chiste) {
this.chiste= chiste;
}
/**
* Devuelve la respuesta del chiste.
* @return Respuesta del chiste
*/public String getRespuesta() {
return respuesta;
}
/**
* Establece la respuesta del chiste.
* @param respuesta Respuesta del chiste
*/publicvoidsetRespuesta(String respuesta) {
this.respuesta= respuesta;
}
@Overridepublicbooleanequals(Object o) {
if (this== o) returntrue;
if (o ==null|| getClass() != o.getClass()) returnfalse;
Chiste chiste = (Chiste) o;
return id == chiste.id;
}
@OverridepublicinthashCode() {
return Objects.hash(id);
}
/**
* Sobrescritura del método toString() para que devuelva el chiste.
* Lo devuelve empleando un StringBuilder y por medio del método forEach() para recorrer la lista de banderas.
* @return Chiste como String
*/@Overridepublic String toString() {
StringBuilder sb =new StringBuilder();
sb.append("Chiste: ").append(chiste).append(System.lineSeparator());
sb.append("Respuesta: ").append(respuesta).append(System.lineSeparator());
sb.append("Categoría: ").append(categoria).append(System.lineSeparator());
sb.append("Idioma: ").append(lenguaje).append(System.lineSeparator());
sb.append("Tipo: ").append(tipo).append(System.lineSeparator());
sb.append("Banderas: ");
banderas.forEach(b -> sb.append(b).append(" "));
sb.append(System.lineSeparator());
return sb.toString();
}
}
B) El adapter ChisteDeserializer:
Detalle de implementación de la clase ChisteDeserializer
package com.javhoz.ad.chistes.model;
import com.google.gson.*;
import java.lang.reflect.Type;
/*
{
"error": false,
"category": "Programming",
"type": "twopart",
"setup": "¿Por qué C consigue todas las chicas y Java no tiene ninguna?",
"delivery": "Porque C no las trata como objetos.",
"flags": {
"nsfw": false,
"religious": false,
"political": false,
"racist": false,
"sexist": false,
"explicit": false
},
"safe": true,
"id": 6,
"lang": "es"
}
*/publicclassChisteDeserializerimplements JsonDeserializer<Chiste> {
@Overridepublic Chiste deserialize(JsonElement elemento, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
// Comprobación si es un objeto:if (!elemento.isJsonObject())
returnnull;
// Creo un chiste vacío, al que le daré valor a sus atributos: Chiste chiste =new Chiste();
// Recupero el objeto JSON del chiste JsonObject jsonChiste = elemento.getAsJsonObject();
// Comprobación de que no hay error en la petición:if (jsonChiste.get("error") !=null&& jsonChiste.get("error").getAsBoolean()) {
returnnull;
}
// Compruebo que cada elemento del objeto existe y lo asigno al objeto Chiste:// La comprobación se hace con el método get() de la clase JsonObject que devuelve// un JsonElement. Si es null, no existe el elemento.if (jsonChiste.get("category") !=null) {
chiste.setCategoria(jsonChiste.get("category").getAsString());
}
if (jsonChiste.get("type") !=null) {
chiste.setTipo(jsonChiste.get("type").getAsString());
}
// En realidad, dependiendo del tipo de chiste, el setup o el delivery pueden no existir.// Por lo que podría hacer comprobando el valor de type, pero lo dejo así para que veáis// como se puede hacer con el método get() de la clase JsonObject.if (jsonChiste.get("setup") !=null) {
chiste.setChiste(jsonChiste.get("setup").getAsString());
if (jsonChiste.get("delivery") !=null) {
chiste.setRespuesta(jsonChiste.get("delivery").getAsString());
}
} elseif (jsonChiste.get("joke") !=null) {
chiste.setChiste(jsonChiste.get("joke").getAsString());
}
if (jsonChiste.get("lang") !=null) {
chiste.setLenguaje(jsonChiste.get("lang").getAsString());
}
if (jsonChiste.get("id") !=null) {
chiste.setId(jsonChiste.get("id").getAsInt());
}
if (jsonChiste.get("flags") !=null) {
JsonObject flags = jsonChiste.get("flags").getAsJsonObject();
if (flags.get("nsfw").getAsBoolean()) {
chiste.addFlag(Flag.NSFW);
}
if (flags.get("religious").getAsBoolean()) {
chiste.addFlag(Flag.RELIGION);
}
if (flags.get("political").getAsBoolean()) {
chiste.addFlag(Flag.POLITICAL);
}
if (flags.get("racist").getAsBoolean()) {
chiste.addFlag(Flag.RACIST);
}
if (flags.get("sexist").getAsBoolean()) {
chiste.addFlag(Flag.SEXIST);
}
if (flags.get("explicit").getAsBoolean()) {
chiste.addFlag(Flag.EXPLICIT);
}
}
return chiste;
}
}
C) La clase ChisteTypeAdapter:
Detalle de implementación de la clase ChisteTypeAdapter
package com.javhoz.ad.chistes.model;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
/*
Formato de JSON:
{
"id": 1,
"category": "Programming",
"type": "single",
"joke": "Chuck Norris can write multithreaded applications with a single thread.",
"flags": {
"nsfw": false,
"religious": false,
"political": false,
"racist": false,
"sexist": false
},
"lang": "en"
*//**
* Updated by javhoz on 16/01/2025.
* Clase que adaptará el tipo Chiste para que pueda ser serializado y deserializado por Gson.
*
* @see com.google.gson.Gson
* @see com.google.gson.TypeAdapter
* @see com.google.gson.GsonBuilder
* @see com.google.gson.JsonDeserializer
*/publicclassChisteTypeAdapterextends TypeAdapter<Chiste> {
@Overridepublicvoidwrite(JsonWriter jsonWriter, Chiste chiste) throws IOException {
jsonWriter.beginObject();
jsonWriter.name("id").value(chiste.getId());
jsonWriter.name("category").value(chiste.getCategoriaString());
jsonWriter.name("type").value(chiste.getTipoString());
if (chiste.getTipo() == TipoChiste.SINGLE) {
jsonWriter.name("joke").value(chiste.getChiste());
} else {
jsonWriter.name("setup").value(chiste.getChiste());
jsonWriter.name("delivery").value(chiste.getRespuesta());
}
jsonWriter.name("flags");
jsonWriter.beginObject();
// Recorremos todas las banderas y asignamos el valor verdadero o falso si el chiste la contiene o no, respectivamente.// Puede hacerse por medio del método containsFlag() de la clase Chiste o recoger las banderas// del chiste e invocar el método contains() de la clase List.for (Flag flag : Flag.values()) {
jsonWriter.name(flag.getNombre().toLowerCase()).value(chiste.containsFlag(flag));
}
jsonWriter.endObject();
jsonWriter.name("lang").value(chiste.getLenguajeString());
jsonWriter.endObject();
}
/**
* Método que deserializa un objeto Chiste a partir de un JsonReader.
*
* @param reader JsonReader que contiene el objeto Chiste
* @return Objeto Chiste
* @throws IOException Si hay un error de E/S
* @see com.google.gson.stream.JsonReader
* @see com.google.gson.stream.JsonToken
*/@Overridepublic Chiste read(JsonReader reader) throws IOException {
if(reader.peek()== JsonToken.NULL|| reader.peek()!= JsonToken.BEGIN_OBJECT){
// reader.nextNull();returnnull;
}
reader.beginObject();
Chiste chiste =new Chiste();
while (reader.peek() != JsonToken.END_OBJECT) {
String name = reader.nextName();
switch (name) {
case"id"-> chiste.setId(reader.nextInt());
case"category"-> chiste.setCategoria(Categoria.getCategoria(reader.nextString()));
case"type"-> chiste.setTipo(TipoChiste.getTipoChiste(reader.nextString()));
case"joke", "setup"-> chiste.setChiste(reader.nextString());
case"delivery"-> chiste.setRespuesta(reader.nextString());
case"flags"->// Para hacerlo más modular he puesto el código en un método aparte. readFlags(reader, chiste);
case"lang"-> chiste.setLenguaje(reader.nextString());
default-> reader.skipValue();
}
}
reader.endObject();
return chiste;
}
privatevoidreadFlags(JsonReader reader, Chiste chiste) throws IOException {
reader.beginObject();
while (reader.peek() != JsonToken.END_OBJECT) {
String flagName = reader.nextName();
switch (flagName) {
case"nsfw"-> {
if (reader.nextBoolean()) chiste.addFlag(Flag.NSFW);
}
case"religious"-> {
if (reader.nextBoolean()) chiste.addFlag(Flag.RELIGION);
}
case"political"-> {
if (reader.nextBoolean())
chiste.addFlag(Flag.POLITICAL);
}
case"racist"-> {
if (reader.nextBoolean())
chiste.addFlag(Flag.RACIST);
}
case"sexist"-> {
if (reader.nextBoolean())
chiste.addFlag(Flag.SEXIST);
}
case"explicit"-> {
if (reader.nextBoolean())
chiste.addFlag(Flag.EXPLICIT);
}
default-> reader.skipValue();
}
}
reader.endObject();
}
}
D) La interface IChisteDAO y clase ChisteDAO se usa para obtener los chistes de la API:
Detalle de implementación de la interfaz IChisteDAO
Podrías realizar mejoras en el código, como la gestión de excepciones, la comprobación de valores nulos, la simplificación de código, etc.
Detalle de implementación de la clase ChisteDAO
package com.javhoz.ad.chistes.model;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Objects;
/**
* Created by Pepe Calo on 07/11/2023
* Implementación de la interfaz IChisteDAO que consulta un chiste en un archivo Json
* mediante la librería Gson.
* La API de chistes utilizada es:
* <a href="https://v2.jokeapi.dev/joke/">...</a>
*
* @see IChisteDAO
* @see Chiste
* @see Gson
* @see GsonBuilder
* @see com.google.gson.JsonObject
* @see com.google.gson.JsonParser
*/publicclassChisteDAOimplements IChisteDAO {
privatefinal Gson gson;
// https://v2.jokeapi.dev/joke/Programming,Miscellaneous?blacklistFlags=nsfw,religiousprivatestaticfinal String BASE_URL ="https://v2.jokeapi.dev/joke/";
privatestaticfinal String ENDPOINT ="?format=json";
privatestaticfinalint NO_ID = 0;
privatestaticfinal String SINGLE ="single";
/**
* Constructor de la clase ChisteDAO.
* Si deseas emplear las clases ChisteSerializer y ChisteDeserializer, debes comentar la línea con ChisteTypeAdapter
* y no comentar las de los otros dos adaptadores.
*/publicChisteDAO() {
gson =new GsonBuilder().setPrettyPrinting()
// .registerTypeAdapter(Chiste.class, new ChisteDeserializer())// .registerTypeAdapter(Chiste.class, new ChisteSerializer()) .registerTypeAdapter(Chiste.class, new ChisteTypeAdapter())
.create();
}
private String getURL(String categoria, String[] tipo, String[] banderas, String idioma, int id) {
String url = BASE_URL + categoria + ENDPOINT;
if (tipo !=null&& tipo.length> 0) {
// Concateno los elementos no nulos media stream de un array de String. En el caso de que no haya ninguno, devuelvo un Optional vacío. String tipos = Arrays.stream(tipo).filter(Objects::nonNull).reduce((s, s2) -> s +","+ s2).orElse(null);
if(tipos!=null&&!tipos.isEmpty()){
url +="&type="+ tipos;
}
}
if (banderas !=null&& banderas.length> 0) {
String flags = Arrays.stream(banderas).filter(Objects::nonNull).reduce((s, s2) -> s +","+ s2).orElse(null);
if(flags!=null&&!flags.isEmpty()){
url +="&blacklistFlags="+ flags;
}
}
if (idioma !=null&&!idioma.isEmpty()) {
url +="&lang="+ idioma;
}
if (id > 0) {
url +="&idRange="+ id;
}
System.out.println("url = "+ url);
return url;
}
private Chiste getJoke(String url) {
try (BufferedReader is =new BufferedReader(new InputStreamReader(new URI(url).toURL().openStream()))) {
return gson.fromJson(is, Chiste.class);
} catch (MalformedURLException e) {
System.err.println("Error en la URL: "+ e.getMessage());
} catch (IOException e) {
System.err.println("Erro E/S: "+ e.getMessage());
} catch (URISyntaxException e) {
thrownew RuntimeException(e);
}
returnnull;
}
private String getJokeAsString(String url) {
Chiste chiste = getJoke(url);
return (chiste!=null) ? chiste.getChiste() + System.lineSeparator() + chiste.getRespuesta() : "";
}
@Overridepublic String getJokeAsString(String categoria, String[] tipo, String[] banderas) {
return getJokeAsString(getURL(categoria, tipo, banderas, null, NO_ID));
}
@Overridepublic Chiste getJoke(String categoria, String[] tipo, String[] banderas) {
return getJoke(getURL(categoria, tipo, banderas, null, NO_ID));
}
@Overridepublic String getJokeAsString(String categoria, String[] tipo, String[] banderas, String idioma) {
return getJokeAsString(getURL(categoria, tipo, banderas, idioma, NO_ID));
}
@Overridepublic Chiste getJoke(String categoria, String[] tipo, String[] banderas, String idioma) {
return getJoke(getURL(categoria, tipo, banderas, idioma, NO_ID));
}
@Overridepublic Chiste getJokeById(int id) {
return getJoke(getURL("Any", null, null, null, id));
}
@OverridepublicvoidsaveJokeAsJson(Chiste chiste, Writer writer) {
gson.toJson(chiste, writer);
}
@Overridepublic String getRandomJokeAsString() {
System.out.println(BASE_URL +"Any");
return getJokeAsString(BASE_URL +"Any");
}
@Overridepublic Chiste getRandomJoke() {
return getJoke(BASE_URL +"Any");
}
}
Ejercicio
Crear una base de datos con JPA y Hibernate para la aplicación JokeAPI y transfiere todos los datos de JSON a la base de datos.
Añade las dependencias necesarias y el fichero de configuración persistence.xml en el directorio META-INF de src/main/resources:
<?xml version="1.0" encoding="UTF-8"?><persistencexmlns="https://jakarta.ee/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"version="3.0"><persistence-unitname="chistesH2"transaction-type="RESOURCE_LOCAL"><provider>org.hibernate.jpa.HibernatePersistenceProvider</provider><!-- <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>--><exclude-unlisted-classes>false</exclude-unlisted-classes><properties><propertyname="jakarta.persistence.jdbc.url"value="jdbc:h2:RutaABaseDatos;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO"/><propertyname="jakarta.persistence.jdbc.user"value=""/><propertyname="jakarta.persistence.jdbc.password"value=""/><propertyname="jakarta.persistence.jdbc.driver"value="org.h2.Driver"/><!-- Automáticamente, genera el esquema de la base de datos --><propertyname="jakarta.persistence.schema-generation.database.action"value="drop-and-create"/><!-- Muestra por pantalla las sentencias SQL --><propertyname="hibernate.show_sql"value="false"/><propertyname="hibernate.format_sql"value="true"/><propertyname="hibernate.highlight_sql"value="true"/><propertyname="hibernate.dialect"value="org.hibernate.dialect.H2Dialect"/></properties></persistence-unit></persistence>
Para ello, crea las siguientes clases:
A) ChisteJpaManager que empleando el patrón Singleton, gestione la creación de la factoría de entidades y el EntityManager.
B) Chiste que emplea JPA para mapear la clase Chiste con la tabla Chiste de la base de datos.
Solución de Chiste
package com.javhoz.ad.chistes.model;
import jakarta.persistence.*;
import java.util.ArrayList;
@EntitypublicclassChisteimplements java.io.Serializable {
@Id@Column(name ="idChiste")
privateint id;
private Categoria categoria;
private TipoChiste tipo;
// Como se trata de una relación muchos a muchos, se emplea la anotación @ElementCollection// H2 admite el tipo de dato Array de enteros (TINYINT ARRAY), prueba a no poner la anotación @ElementCollection ni @CollectionTable@ElementCollection// Para que se cree una tabla intermedia@Enumerated(EnumType.STRING)
@CollectionTable(name ="FlagsChiste", joinColumns =@JoinColumn(name ="idChiste"))
privatefinal List<Flag> banderas;
private String chiste;
private String respuesta;
private Lenguaje lenguaje;
//...}
C)ChisteDownloader que descarga los chistes de la API y los guarda en la base de datos.
Ten el cuenta que ChisteDownloader es un Singleton y que se puede configurar el número de chistes a descargar, además de un tiempo de espera entre chiste y chiste (la API sólo permite 120 peticiones por minuto).
Por ello, haz que sea un hilo que se ejecute cada cierto tiempo ( implements Runnable ) y tenga los siguientes atributos:
tiempoEspera que es el tiempo de espera entre chiste y chiste.
instance que es la instancia de ChisteDownloader.
MAX_CHISTES que es el número máximo de chistes a descargar.
chisteDAO que es el DAO de Chiste.
numeroChistes que es el número de chistes a descargar (si no se indica debe ser MAX_CHISTES).
Ejercicio 05.01. Acceso combinado a la entidad Chiste.
Mofifica la entidad Chiste para que guarde el chiste y la respuesta en un solo campo en la base de datos, pero que se muestren por separado en la aplicación.
Ejercicio 05.02. CLOB y BLOB de una entidad Documento
Crea una entidad Documento que tenga un campo de texto grande (CLOB) para el contenido del documento y un campo de bytes grande (BLOB) para la imagen del documento. Haz pruebas con tres gestores de bases de datos: H2, SQLite y PostgreSQL y comprueba el resultado creando la tabla en cada uno de ellos, con y sin declaración de tipo de LOB.
Ejercicio 05.03. Conversores personalizados y enumeraciones
Declara una entidad Persona con atributos:
idPersona.
nombre.
apellidos.
fechaNacimiento de tipo LocalDate.
sexo de tipo enumerado Sexo que puede ser HOMBRE o MUJER.
estadoCivil de tipo enumerado EstadoCivil que puede ser SOLTERO, CASADO, DIVORCIADO o VIUDO.
foto de tipo byte[].
Realiza las conversiones para que:
El nombre y apellidos se guardan en la base de datos como “apellido1, nombre”, con la primera letra de cada palabra en mayúsculas (empleando acceso por campo y por propiedad).
La fecha de nacimiento como un entero que representa la edad de la persona en años (obviamente no es la mejor forma de almacenar la edad, pero quiero que practiquéis con los convertidores), usando anotaciones @PostLoad y @PrePersist. Haz pruebas de comportamiento haciendo consultas, inserciones y actualizaciones.
Las enumeraciones se guardarán como cadenas en el caso de estado civil y como un carácter de ‘H’ o ‘M’ en el caso del sexo. Hazlo con conversores personalizados.
La fotografia se guardará en un campo de tipo BLOB.
Debes completar la entidad Persona y los convertidores necesarios para que funcione correctamente.
Hazlo contra la base de datos H2 y comprueba que los datos se guardan correctamente, creando varios registros y recuperándolos.
Ejercicio 05.04. Generación de ids con tabla
A partir del ejecicio anterior con Persona, haz aque el campo idPersona de tipo Long y genera el identificador con una tabla.
La tabla debe ser compartida con otras entidades que tengan un campo id de tipo Long.
Nombre de la tabla: LONG_ID_GEN
Columnas:
nomePK.
valorPK.
El valor de la columna nomePK para la entidad Persona debe ser PERSONA_ID.
Dale un valor inicial de 1000 y un tamaño de asignación de 100.
Crea otro generador para esa tabla que se utilizará para la entidad Direccion con un valor inicial de 2000 y un tamaño de asignación de 50.
Haz pruebas de inserción de datos.
Ejercicio 05.05. Generación de ids con una secuencia
Repite el ejercicio anterior con Persona, pero esta vez utiliza una secuencia para generar el identificador en una base de datos H2. Haz pruebas compartiendo la secuencia y sin compartirla.
Si puedes, haz lo mismo con una base de datos PostgreSQL.
Ejercicio 05.06. Ampliación de la aplicación de persistencia de una biblioteca
Amplía el ejercicio de la biblioteca para que la entidad Book tenga un identificador generado automáticamente por medio de una tabla.
Además:
Crea una enumeración llamada Categoría con los siguientes valores: NOVELA, POESIA, ENSAYO, TEATRO y OTROS.
Haz que la entidad Book tenga un atributo de tipo Categoría y que se persista en la base de datos como una cadena. Realiza una conversión de la enumeración a cadena y viceversa de modo que guarde la categoría con el nombre en mayúsculas sólo la primera letra y con acentos.
Haz que la columna ISBN sea única, de un tamaño de 13 caracteres y que no pueda ser nula.
Crea un atributo de tipo Calendar para la fecha de publicación del libro y haz que se persista en la base de datos como un tipo DATE.
Crea un atributo transitorio que sea el número de días que han pasado desde la fecha de publicación hasta la fecha actual. Utiliza la clase java.time.LocalDate para obtener la fecha actual.
Crea otro atributo transitorio con el ISBN en versión de 10 dígitos, teniendo en cuenta que el ISBN es un número de 13 dígitos. Para ello, puedes utilizar la clase java.math.BigInteger para realizar la conversión y el siguiente algoritmo:
Elimina los primeros tres dígitos (normalmente 978)
Elimina el último dígito. Ahora tienes nueve dígitos
Ahora necesitas calcular el ‘dígito de control’, que será el décimo dígito de tu ISBN. El objetivo del dígito de control es asegurarse de no haber cometido un error tipográfico: transponer dos dígitos, por ejemplo, o escribir mal uno. Esto es bastante complicado:
Multiplica el primer dígito por 10, el segundo por 9, el tercero por 8 y así sucesivamente, hasta llegar al último dígito (multiplicado por 2).
Ahora tienes una cadena de 9 números nuevos. Agrégalos todos juntos.
Divide esta suma por once. Ahora estás interesado en el resto. Por ejemplo, si la suma fuera 242, que es exactamente 11 x 22, entonces el resto es cero. Si la suma fuera 243, entonces sobraría 1. Tendrás un resto que está entre 0 y 10.
Resta ese resto de 11 para obtener el dígito de control.
Si el resultado es 10, entonces el dígito de control es ‘X’.
Código Java:
publicclassISBN {
publicstaticvoidmain(String[] args) {
String isbn ="978-3-16-148410-0";
String isbn10 = isbn.substring(3, isbn.length() - 1);
System.out.println(isbn10);
BigInteger sum = BigInteger.ZERO;
for (int i = 0; i < isbn10.length(); i++) {
int digit = Character.getNumericValue(isbn10.charAt(i));
sum = sum.add(BigInteger.valueOf(digit).multiply(BigInteger.valueOf(10 - i)));
}
System.out.println(sum);
BigInteger remainder = sum.mod(BigInteger.valueOf(11));
System.out.println(remainder);
BigInteger controlDigit = BigInteger.valueOf(11).subtract(remainder);
System.out.println(controlDigit);
if (controlDigit.intValue() == 10) {
System.out.println("X");
} else {
System.out.println(controlDigit);
}
}
}
Un ejemplo más completo:
publicclassISBNConverter {
publicstaticvoidmain(String[] args) {
String isbn13 ="9780123456789"; // ISBN-13 String isbn10 = convertirISBN13aISBN10(isbn13);
System.out.println("ISBN-10: "+ isbn10);
}
publicstatic String convertirISBN13aISBN10(String isbn13) {
// Verifica si el ISBN-13 proporcionado es válidoif (!esISBN13Valido(isbn13)) {
return"ISBN-13 no válido";
}
// Elimina los primeros 3 dígitos (978 o 979) del ISBN-13 String isbn10Parcial = isbn13.substring(3);
// Calcula el dígito de verificación para el ISBN-10 parcialint suma = 0;
for (int i = 0; i < 9; i++) {
int digito = Character.getNumericValue(isbn10Parcial.charAt(i));
suma += (i + 1) * digito;
}
int digitoVerificador = suma % 11;
char digitoVerificadorChar;
if (digitoVerificador == 10) {
digitoVerificadorChar ='X';
} else {
digitoVerificadorChar = (char) ('0'+ digitoVerificador);
}
// Combina el ISBN-10 parcial con el dígito de verificación calculadoreturn isbn10Parcial + digitoVerificadorChar;
}
publicstaticbooleanesISBN13Valido(String isbn13) {
// Verifica que el ISBN-13 tenga 13 dígitos y comience con "978" o "979"return isbn13.matches("^97[89]\\d{10}$");
}
}
Crea varios libros y pérsistelos en la base de datos (una nueva). Recupéralos y muestra los valores de los datos, incluyendo transitorios.
Ejercicio 06.01. Relación uno a uno bidireccional Equipo-Entrenador
Vamos a crear una aplicación de equipos de la NBA. Cada equipo tiene un entrenador y cada entrenador tiene un equipo, por lo que la relación es uno a uno bidireccional.
Crea las siguientes entidades:
Equipo: con los atributos idEquipo, nombre, ciudad, conferencia, division, nombreCompleto y abreviatura.
Crea una enumeración Conferencia con los valores ESTE y OESTE.
Crea una enumeración Division con los valores ATLANTICO, CENTRAL, SURESTE, NOROESTE, PACIFICO y SUROESTE.
En la base de datos, la conferencia y la división se guardarán como cadenas:
Entrenador: con los atributos idEntrenador, nombre, fechaNacimiento, salario y equipo.
Mediante JPA e Hibernate, crea una aplicación que permita:
Añadir un equipo.
Insertar un entrenador.
Asignar un entrenador a un equipo.
Asignar un equipo a un entrenador.
Mostrar los datos de un equipo y su entrenador.
Para ello, debes crear las clases de utilidad necesarias para realizar las operaciones anteriores.
JpaNbaManager, EquipoDAO, EntrenadorDAO, etc.
Ejercicio 06.02. Relación muchos a uno unidireccional Jugador-Equipo
Siguiendo el ejemplo anterior, vamos a crear una relación muchos a uno unidireccional entre Jugador y Equipo.
Para ello debe crear una nueva entidad Jugador con los siguientes atributos:
idJugador: identificador del jugador.
nombre: nombre del jugador.
apellidos: apellidos del jugador.
equipo: equipo al que pertenece el jugador.
altura: altura del jugador (Double).
peso: peso del jugador (Double).
numero: número de camiseta del jugador (SmallInt).
anoDraft: año de elección en el draft (entero).-
numeroDraft: número de elección en el draft (SmallInt).
rondaDraft: ronda de elección en el draft (SmallInt).
posicion: posición en la que juega (base, escolta, alero, ala-pívot, pívot, como enumeración, que debe guardarse como ‘G’, ‘C’, ‘F’, ‘F-C’, ‘C-F’).
pais: país de origen del jugador.
colegio: universidad o equipo en el que jugó.
foto: foto del jugador.
Haz que la relación sea unidireccional, de modo que la entidad Jugador tenga una referencia al Equipo y el nombre de la clave foránea sea idEquipo.
Crea jugadores y añádelos a los equipos que has creado en el ejercicio anterior. Completa la aplicación para que puedas añadir jugadores a los equipos y mostrar los jugadores de un equipo.
Ejercicio 06.03. Relación muchos a muchos unidireccional Jugador-Posición
Vamos a crear una relación muchos a muchos unidireccional entre Jugador y Posicion. Para eso debes crear una nueva entidad Posicion con los siguientes atributos:
idPosicion: identificador de la posición (Long).
nombre: nombre de la posición (String, tamaño máximo 50).
abreviatura: abreviatura de la posición (String, tamaño máximo 3).
descripcion: descripción de la posición (String, tamaño máximo 255).
Haz que la relación sea unidireccional, de modo que la entidad Jugador tenga una colección de Posicion y el nombre de la tabla de unión sea JugadorPosicion.
Crea posiciones y añádelas a los jugadores que has creado en el ejercicio anterior.
Ejercicio 06.04. Mapeo de una base de datos de juegos
Migración de base de datos H2 entre versiones
En la base de datos origen se ejecuta el siguiente script:
SCRIPT TO'<ruta-al-archivo-backup>/backup.sql';
En la base de datos destino se ejecuta el siguiente script:
Cuyos datos se ajustan al formato del siguiente JSON (ejemplo). Debes tener en cuenta que no se ha creado la tabla de requeriminetos mínimos, pero se puede hacer si se desea en una nueva tabla de la base de datos, relacionada, uno a uno:
{
"id": 452,
"title": "Call Of Duty: Warzone",
"thumbnail": "https:\/\/www.freetogame.com\/g\/452\/thumbnail.jpg",
"status": "Live",
"short_description": "A standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare.",
"description": "Call of Duty: Warzone is both a standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare. Warzone features two modes \u2014 the general 150-player battle royle, and \u201cPlunder\u201d. The latter mode is described as a \u201crace to deposit the most Cash\u201d. In both modes players can both earn and loot cash to be used when purchasing in-match equipment, field upgrades, and more. Both cash and XP are earned in a variety of ways, including completing contracts.\r\n\r\nAn interesting feature of the game is one that allows players who have been killed in a match to rejoin it by winning a 1v1 match against other felled players in the Gulag.\r\n\r\nOf course, being a battle royale, the game does offer a battle pass. The pass offers players new weapons, playable characters, Call of Duty points, blueprints, and more. Players can also earn plenty of new items by completing objectives offered with the pass.",
"game_url": "https:\/\/www.freetogame.com\/open\/call-of-duty-warzone",
"genre": "Shooter",
"platform": "Windows",
"publisher": "Activision",
"developer": "Infinity Ward",
"release_date": "2020-03-10",
"freetogame_profile_url": "https:\/\/www.freetogame.com\/call-of-duty-warzone",
"minimum_system_requirements": {
"os": "Windows 7 64-Bit (SP1) or Windows 10 64-Bit",
"processor": "Intel Core i3-4340 or AMD FX-6300",
"memory": "8GB RAM",
"graphics": "NVIDIA GeForce GTX 670 \/ GeForce GTX 1650 or Radeon HD 7950",
"storage": "175GB HD space" },
"screenshots": [
{
"id": 1124,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-1.jpg" },
{
"id": 1125,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-2.jpg" },
{
"id": 1126,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-3.jpg" },
{
"id": 1127,
"image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-4.jpg" }
]
}
a) Crea entidades JPA en Java para las tablas de la base de datos, con las siguientes características:
Genero: con los atributos idGenero, nombre. La clave es autonumérica.
Plataforma: con los atributos idPlataforma y nombre. La clave es autonumérica. Nota: si se hubiese declarado como enumeración, para poder mapear una enumeración en una tabla independiente, obligaría a crear una entidad independiente con el idPlataforma y el nombre. Sin embargo, en este caso, se podría mapear la enumeración directamente en la tabla Juego o declararla como una clase y no como una enumeración.
Juego: con todos los atributos de la tabla Juego, incluyendo la relación con Genero y Plataforma. La clave primaria, idJuego, no es autogenerada, es asignada. Ten en cuenta que la relación con la tabla Imagen se trata de una relación uno a muchos, por lo que se deberá declarar una colección de imágenes. Además, el idGenero y el idPlataforma son claves foráneas de las entidades y no deben declararse como atributos de la entidad Juego, sino como objetos del tipo de las entidades Genero y Plataforma.
Imagen: con los atributos idImagen (no autogenerada), Juego (relacionada con la entidad Juego @OneToOne), url, imagen (tipo byte[]).
RequisitosSistema: relacionada con la tabla Juego. Atributos: idJuego (PK), sistemaOperativo (su nombre no coincide con la columna de la tabla), almacenamiento, graficos, memoria, procesador y su relación juego. Debe emplearse una clave comparta con el idJuego. Para ello debe emplearse la anotación: @MapsId: @MapsId("idJuego").
b) Haz una sencilla aplicación que cree un juego y lo persista en la base de datos. Ten en cuenta que las claves no son autonuméricas
Ejemplo de juegos: https://www.freetogame.com/api/game?id=X, pasándole el id del Juego, desde 1 al número de juegos que consideres. Ten en cuenta que el juego podría no existir devolviendo:
{"status":0,"status_message":"No game found with that id"}
Bases de datos de videojuegos (H2)
Ejercicio 06.05. Claves compartidas en relaciones uno a uno
Comprueba el funcionamiento de la anotación @PrimaryKeyJoinColumn en una relación uno a uno entre Persona y Departamento. Crea las entidades y realiza pruebas de persistencia.
Persona: idPersona (IDENTITY), nombre, departamento (uno a uno con anotación de @PrimaryKeyJoinColumn)
Departamento: idDepartamento (IDENTITY), nombre.
Modifica el ejercicio para que sea bidireccional con @OneToOne y @MapsId en la entidad Departamento y como propietaria de la relación.
Ejercicio 07.01. Elementos embebidos.
Crea una aplicación con JPA para la gestión de películas y series.
Crea una clase InfoContenido con los siguientes atributos:
titulo (String): de tamaño 100.
genero (String): de tamaño 50.
pais (String): de tamaño 2.
duracion (int): duración en minutos.
año (int): año.
sinopsis (String): de tamaño clob.
Crea una entidad Serie con los siguientes atributos:
idSerie (long): identificador de la serie. Secuencia.
informacion (de tipo InfoContenido)
fechaEstreno (LocalDate).
temporadas (int): número de temporadas.
capitulos (int)
directores (lista de String).
Crea una entidad Pelicula con los siguientes atributos:
idPelicula (long): identificador de la película. Secuencia.
informacion (de tipo InfoContenido)
La entidad Serie y Pelicula deben tener el atributo informacion como un objeto embebido.
La entidad Pelicula el atributo pais debe ser renombrado a paisPelicula.
El atributo directores debe guardarse en una nueva tabla, como una colección con la anotación @ElementCollection (busca información sobre esta anotación).
La fecha de estreno, fechaEstreno, de la serie debe guardarse en formato numérico (YYYYMMDD).
Ejercicio 07.02. Clave compuesta en una relación de muchos a muchos.
Data la aplicación de gestión de películas y series, añade dos nuevas entidades: Usuario y Calificacion que permita a los usuarios calificar las películas.
Crea una clase Usuario con los siguientes atributos:
idUsuario (long): identificador del usuario. Secuencia.
nombre (String): nombre del usuario.
email (String): email del usuario.
password (String): contraseña del usuario.
fechaRegistro (LocalDate): fecha de registro.
Crea una clase Calificacion con los siguientes atributos:
calificacion (int): calificación del contenido, con valores de 10 a 0.
fechaCalificacion (LocalDate): fecha de la calificación.
comentario (String): comentario de la calificación.
Además, debe estar relacionado con las entidades Usuario, Pelicula y Serie. Como un usuario puede calificar varias películas y series, y una película o serie puede ser calificada por varios usuarios, es una relación de muchos a muchos. No es preciso que califique series, pues el caso de uso es similar al de las películas.
La clave primaria de la tabla Calificacion debe ser compuesta por los atributos idUsuario, idPelicula.
Ejercicio 07.03. Entidades principales de base de datos de películas.
El título de la película se guarda en el campo castelan.
El identificador de la película es entero (no autoincremento).
Los participantes de la película están relacionados por medio da de la tabla peliculapersonaxe, en la que el campo ocupacion identifica o tipo de ocupación de la película (‘Actor’, …):
Para empezar, crea las entidades Pelicula, Personaxe y Ocupacion.
A continuación, crea la entidad PeliculaPersonaxe que relaciona las entidades Pelicula y Personaxe y Ocupacion. Ten en cuenta que tiene un nuevo atributo personaxeInterpretado, que es el nombre del personaje interpretado por el actor en la película.
Mejora:
Crea las entidades asociadas a la base de datos. De modo que las columnas anoInicio, outrasDuracions, video, laserDisc pertenezca a una entidad DetallePelicula de tipo embebido.
Hay que tener en cuenta que estos elementos no siempre están presentes, por lo que deben ser opcionales.
Ejercicio 09.01. Ejecución de consultas JPA Películas
Implementa el ejemplo anterior contra una base de datos de películas proporcionada.
URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas
URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false
Pese a todo lo mejor es que el usuario y contraseña se guarden en un archivo de propiedades, persistence.xml, como se ha visto en el tema de configuración.
Realiza las siguientes consultas:
Las películas que no tienen año de inicio definido:
SELECT p.castelan, p.anoFin, p.anoInicio
FROM Pelicula p
WHERE p.anoInicio ISNOTNULL;
Las películas con una duración superior a 120 minutos:
SELECT p.castelan, p.anoFin, p.duracion
FROM Pelicula p
WHERE p.duracion >120;
Las películas con Antonio Banderas:
SELECT p FROM Pelicula p JOIN p.personaxes pp JOIN pp.personaxe per WHERE per.nomeOrdenado LIKE'Antonio Banderas';
Las tablas de la base de datos sobre las que hay que implantar las entidades y realizar las consultas son las del ejercicio anterior.
Ejercicio 09.02. Creación de consultas JPA Películas
a) Obtener todas las películas que tienen una duración mayor a 120 minutos.
b) Obtener todas las películas que pertenecen a un género específico (por ejemplo, “Drama”).
c) Obtener todas las ocupaciones que tienen más de 5 películas asociadas.
d) Obtener todas las películas que tienen un país específico (por ejemplo, “España”).
e) Obtener todas las películas que tienen al menos un personaje interpretado por un actor de un país específico (por ejemplo, “Francia”).
f) Obtener todas las películas que tienen música compuesta por un compositor específico (por ejemplo, “John Williams”).
g) Obtener todas las películas que tienen un personaje interpretado por un actor con un nombre específico (por ejemplo, “Tom Hanks”).
h) Obtener todas las películas que tienen un género específico y que fueron producidas en un año específico (por ejemplo, “Acción” y 2005).
i) Obtener todas las películas que tienen un personaje interpretado por un actor de un género específico (por ejemplo, “Mujer”).
f) Obtener todas las películas que tienen un personaje interpretado por un actor que nació en un país específico y que tienen una duración mayor a 100 minutos.
g) Devolver todos los países que no tienen películas asociadas, puedes usar una consulta JPQL que utilice una subconsulta o un LEFT JOIN con una condición IS NULL.
Ejercicio 09.03. Consultas SQL a JPQL (películas)
Amplía el ejercicio anterior la base de datos de películas proporcionada.
URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas
URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false
Pese a todo lo mejor es que el usuario y contraseña se guarden en un archivo de propiedades, persistence.xml, como se ha visto en el tema de configuración.
Las tablas de la base de datos sobre las que hay que implantar las entidades y realizar las consultas son las del ejercicio anterior.
Realiza las siguientes consultas:
Muestra la película solicitando el id:
SELECT castelan, orixinal, anoFin, poster ISNOTNULLas tenPoster
FROM pelicula WHERE idPelicula = :identificador
Muestra las películas que tienen algún personaje (IS EMPTY) o no tienen personajes (IS NOT EMPTY).
Muestra las películas que tienen personajes con una ocupación concreta:
SELECT P.nome FROM peliculapersonaxe PP, personaxe P
WHERE P.idPersonaxe=PP.idPersonaxe ANDPP.ocupacion='OCUPACIÓNCONCRETA'AND PP.idPelicula=IDENTIFICADOR_PELICULA
Muestra los títulos de las películas en las que ha trabajado un actor concreto.
Listar el número de películas de acuerdo con el nombre propocionado:
(Crea una clase PeliculaDTO con los campos idPelicula, castelan, orixinal, anoFin, tenPoster (booleano) y realiza la consulta)
SELECT idPelicula, castelan, orixinal, anoFin, poster ISNOTNULLas tenPoster
FROM pelicula WHERE castelan LIKE‘%:nombre%’ORDERBY5DESC, castelan ASC
Consulta los datos de las ocupaciones de los personajes de una película:
SELECT O.ocupacion FROM ocupacion O WHEREEXISTS (
SELECT idPelicula FROM peliculapersonaxe PP WHEREO.ocupacion=PP.ocupacion
AND PP.idPelicula=IDENTIFICADOR_DE_PELICULA)
AND O.orde<>0ORDERBY O.orde
y los nombres sde los personajes que tienen esa ocupación:
SELECT P.nome FROM peliculapersonaxe PP, personaxe P
WHERE P.idPersonaxe=PP.idPersonaxe ANDPP.ocupacion='OCUPACIÓNCONCRETA'AND PP.idPelicula=IDENTIFICADOR_PELICULA
Ejercicio 11.01. Paginación de películas
Ejercicio. Paginación de películas
Características de la base de datos:
URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas
URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false
Pese a todo lo mejor es que el usuario y contraseña se referencien en el archivo de propiedades, persistence.xml, de modo independiente, como se ha visto en el tema de configuración.
Las tablas de la base de datos y las entidades ya han sido desarrolladas en apartados anteriores.
Se trata de realizar una aplicación que permita consultar las películas por nombre (pide la introducción de un texto) y muestre las películas de la base de datos de 10 en 10. Se debe mostrar el idPelicula, castelan, orixinal, anoFin y el director (relacionado con PeliculaPersonaxe).
Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.
La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.
Crea una clase PeliculaPaginaDTO que tenga los campos idPelicula, castelan, orixinal, anoFin y director.
Crea una clase PeliculaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.
Ejercicio 12.01. Herencia
Crea una aplicación que permita gestionar una base de datos de vehículos. La base de datos tiene las siguientes tablas:
1. Creación de EntityManagerFactory con patrón Singleton y Thread-Safe
1.1. EntityManagerFactory Singleton
Crea un EntityManagerFactory con patrón Singleton y Thread-Safe. La clase debe tener las siguientes características:
Un método estático, getEmFactory, que devuelva una instancia de EntityManagerFactory, recogiendo el nombre de la unidad de persistencia.
Un método estático, getEntityManager, que devuelva una instancia de EntityManager, recogiendo el nombre de la unidad de persistencia.
Un método, isEntityManagerFactoryClosed, que devuelva si la factoría es nula o está cerrada.
Un método para cerrar la factoría.
1.2. EntityManagerFactory Singleton con propiedades
Añade a la clase anterior un método para que el EntityManagerFactory sea creado con un mapa de propiedades que se le pasan al método createEntityManagerFactory() de Persistence. El mapa de propiedades debe tener las siguientes propiedades:
jakarta.persistence.jdbc.url: la URL de la base de datos.
jakarta.persistence.jdbc.user: el usuario de la base de datos.
jakarta.persistence.jdbc.password: la contraseña de la base de datos.
jakarta.persistence.jdbc.driver: el driver de la base de datos.
jakarta.persistence.schema-generation.database.action: la acción de la base de datos.
jakarta.persistence.schema-generation.create-source: la fuente de creación de la base de datos.
1.3. EntityManagerFactory Singleton para cada unidad de persistencia
Mejora: el EntityManager debe ser creado con el método createEntityManager() de la factoría y debe ser único para cada unidad de persistencia.
Para ello, en vez de tener una única instancia de EntityManagerFactory, debes tener un Map de EntityManagerFactory, una para cada unidad de persistencia, en el que la clave sea el nombre de la unidad de persistencia y el valor un objeto de tipo EntityManagerFactory.
2. Base de datos de legislación: Rango Legal y Organismo
Realizar un proyecto JPA con EclipseLink que mapee las tablas de la base de datos muestre todos los rangos legales y organismos de la base de datos.
RangoLegal:
idRangoLegal (Integer), nomeG (String), nomeC (String), descripcion (texto largo). Los nombres de los atributos nomeG y nomeG no coinciden con los de la base de datos y tienen tamaño 128, además, son únicos. La clave primaria es auto numérica.
Organismo:
idOrganismo (Integer), nome (String), descripcion (texto largo). El nombre es único. La clave primaria es autonumérica.
Crea una base de datos en PostgreSQL con el nombre Lexislacion y las tablas RangoLegal y Organismo.
Haz que la aplicación migre los datos de la base de datos de MariaDB a la de PostgreSQL.
3. Alquiler de películas
Crea una base de datos en PostgreSQL con el nombre videoclub y restaura la base de datos db-videoclub.tar.
Dicha base de datos tiene 15 tablas:
actor: almacena datos de actores, incluidos el nombre y el apellido.
film: almacena datos de películas como título, año de lanzamiento, duración, clasificación, etc.
film_actor: almacena las relaciones entre películas y actores.
category: almacena datos de las categorías de las películas.
film_category: almacena las relaciones entre películas y categorías.
store: contiene los datos de la tienda, incluidos el personal gerencial y la dirección.
inventory: almacena datos del inventario.
rental: almacena datos de alquiler.
payment: almacena los pagos de los clientes.
staff: almacena datos del personal.
customer: almacena datos de los clientes.
address: almacena datos de dirección para el personal y los clientes.
city: almacena los nombres de las ciudades.
country: almacena los nombres de los países.
Ahora que conocemos todo sobre nuestra base de datos de videoclub de ejemplo, pasemos a cargar la misma base de datos en el servidor de la base de datos PostgreSQL. Los pasos para ello se enumeran a continuación:
Paso 1: Cree una base de datos de videoclub, abriendo la consola SQL. Una vez que abra la consola, deberás añadir las credenciales necesarias para la base de datos, que se verían algo así:
Servidor [localhost]:
Base de datos [postgres]:
Puerto [5432]:
Nombre de usuario [postgres]:
Contraseña para el usuario postgres:
Ahora, usando la declaración CREATE DATABASE, cree una nueva base de datos de la siguiente manera:
CREATEDATABASE videoclub;
Paso 2: Cargue el archivo de la base de datos creando una carpeta en la ubicación deseada (por ejemplo, C:\users\sample_database\bd-videoclub.tar). Ahora abra el símbolo del sistema y navegue hasta la carpeta bin de la carpeta de instalación de PostgreSQL como se muestra a continuación (en el caso de haber añadido la ruta de instalación de PostgreSQL al PATH no será necesario navegar hasta la carpeta bin):
cd C:\ruta\a\la\carpeta\bin
Use la herramienta pg_restore para cargar datos en la base de datos videoclub que acabamos de crear mediante el siguiente comando:
De momento, mapea el campo fulltext como un String:
Haz que la entidad Pelicula tenga una con la entidad Idioma (un idioma puede tener muchas películas, pero una película sólo puede tener un idioma).
CategoriaPelicula: haz que la entidad Pelicula tenga una relación con la entidad Categoria (una película puede tener muchas categorías y una categoría puede tener muchas películas), para ello, crea una entidad CategoriaPelicula que mapee la tabla film_category, que dispone de las siguientes columnas: film_id (int4), category_id (int4), last_update (timestamp). IMPORTANTE: la clave primaria de la tabla film_category es compuesta por film_id y category_id.
PeliculaActor: haz que la entidad Pelicula tenga una relación con la entidad Actor (una película puede tener muchos actores y un actor pudo haber realizado muchas películas), para ello, crea una entidad PeliculaActor que mapee la tabla film_actor, que dispone de las siguientes columnas: actor_id (int4), film_id (int4), last_update (timestamp). La clave primaria de la tabla film_actor es compuesta por actor_id y film_id.
Ciudad: mapee la tabla city que dispone de las siguientes columnas: city_id (serial4), city (varchar(50)), country_id (int4), last_update (timestamp). Haz que la entidad Ciudad tenga una relación con la entidad Pais (una ciudad pertenece a un único país y un país puede tener muchas ciudades).
Direccion: mapee la tabla address que dispone de las siguientes columnas: address_id (serial4), address (varchar(50)), address2 (varchar(50)), district (varchar(20)), city_id (int4), postal_code (varchar(10)), phone (varchar(20), last_update (timestamp). Haz que la entidad Direccion tenga una relación con la entidad Ciudad (una dirección pertenece a una única ciudad y una ciudad puede tener muchas direcciones).
Empleado: mapee la tabla staff que dispone de las siguientes columnas: staff_id (serial4), first_name (varchar(45)), last_name (varchar(45)), address_id (int4), email (varchar(50)), store_id (int4), active (boolean), username (varchar(16)), password (varchar(40)), last_update (timestamp). Haz que la entidad Empleado tenga una relación con la entidad Direccion (un empleado tiene una dirección y una dirección puede pertenecer a muchos empleados) y con la entidad Tienda.
Tienda: mapee la tabla store que dispone de las siguientes columnas: store_id (serial4), manager_staff_id (int4), address_id (int4), last_update (timestamp). Haz que la entidad Tienda tenga una relación con la entidad Direccion (una tienda tiene una dirección y una dirección puede pertenecer a muchas tiendas).
Inventario: mapee la tabla inventory que dispone de las siguientes columnas: inventory_id (serial4), film_id (int4), store_id (int4), last_update (timestamp). Haz que la entidad Inventario tenga una relación con la entidad Pelicula (un inventario tiene una película y una película puede estar en muchos inventarios) y con la entidad Tienda (un inventario pertenece a una tienda y una tienda puede tener muchos inventarios).
Cliente: mapee la tabla customer que dispone de las siguientes columnas: customer_id (serial4), store_id (int4), first_name (varchar(45)), last_name (varchar(45)), email (varchar(50)), address_id (int4), activebool (boolean), create_date (date), last_update (timestamp), active (int4). Haz que la entidad Cliente tenga una relación con la entidad Tienda (un cliente pertenece a una tienda y una tienda puede tener muchos clientes) y con la entidad Direccion (un cliente tiene una dirección y una dirección puede pertenecer a muchos clientes).
Alquiler: mapee la tabla rental que dispone de las siguientes columnas: rental_id (serial4), rental_date (timestamp), inventory_id (int4), customer_id (int4), return_date (timestamp), staff_id (int4), last_update (timestamp). Haz que la entidad Alquiler tenga una relación con la entidad Inventario (un alquiler tiene un inventario y un inventario puede tener muchos alquileres), con la entidad Cliente (un alquiler tiene un cliente y un cliente puede tener muchos alquileres) y con la entidad Staff (un alquiler tien
Pago: mapee la tabla payment que dispone de las siguientes columnas: payment_id (serial4), customer_id (int4), staff_id (int4), rental_id (int4), amount (numeric(5,2)), payment_date (timestamp). Haz que la entidad Pago tenga una relación con la entidad Alquiler (un pago tiene un alquiler y un alquiler puede tener muchos pagos), con la entidad Cliente (un pago tiene un cliente y un cliente puede tener muchos pagos) y con la entidad Staff (un pago tiene un empleado y un empleado puede tener muchos pagos).
Diagrama de la base de datos:
4. Pedidos PostgreSQL
Data la estructura de datos de MariaDB se define en el script bd-pedidos.sql, crea un proyecto JPA con Hibernate que mapee las tablas de la base de datos en PostgreSQL (no crees la base de datos en PostgreSQL, simplemente mapea las tablas, tampoco lo hagas en MariaDB):
Crea un proyecto con JPA y Hibernate tenga las siguientes entidades:
Producto: nombre no nulo. La imagen como bytea.
Cliente: dni y nombre no nulo.
Pedido: fecha no nula.
LineaPedido: cantidad de tipo entero corto y no nula.
Haz que el producto tenga la imagen guardada en la base de datos, no como cadena, de tipo bytea.
Los pedidos deben estar ordenados por fecha y las líneas de pedido por cantidad.
Producto dispone de una colección de elementos con los comentarios del pedido.
LineaPedido debe mapearse como una colección de elementos. Comprueba el resultado y hazlo como entidad.
LineaPedido debe tener una colección de tags.
Las relaciones deben actualizarse y borrarse en cascada.
5. Pedidos
Para este ejercicio usaremos la base de datos MariaDB definida en el script bd-pedidos.sql.
Deberás crear un proyecto JPA con EclipseLink que mapee las tablas de la base de datos, empleando las entidades del ejercicio anterior, pero mapeadas con EclipseLink.
Crea en el mismo archivo de persistencia, una nueva unidad de persistencia que se conecte a la base de datos de MariaDB con EclipseLink.
Migra los datos de la base de datos de PostgreSQL a la de MariaDB, si es que no lo has hecho en el ejercicio anterior.
La aplicación debe permitir hacer lo siguiente:
Mostrar todos los productos de la base de datos.
Mostrar todos los pedidos de un cliente.
Añadir un pedido.
Borrar un pedido.
Para ello, crea una clase AppPedidos con un menú que permita realizar las operaciones anteriores y una clase DAO para cada entidad.
La clase genérica DAO<T, K > recoge el tipo de objeto, el tipo de la clave primaria, que tenga los métodos comunes a todas las entidades. La clase DAO genérica debe tener como atributo un EntityManager. La clase DAO debe tener los métodos necesarios para realizar las operaciones anteriores, así como un atributo de tipo EntityManager.
Los nombres de las tablas son en CamelCase, así como los nombres de los atributos. Ten en cuenta que muchos atributos no coinciden con los de las entidades.
Las entidades/enumeraciones que debes implantar están en amarillo.
Publicacion no es una entidad, es una enumeración, de la aplicación, por lo que no debe ser implantada como entidad (el idPublicacion en Norma coindice con el índice de la enumeración más 1: DOG, BOE, DOCE).
1. JPAUtil
Clase que implanta el patrón Singleton con doble comprobación para obtener el objeto de tipo EntityManager.
Las relaciones de la entidad Norma con RangoLegal y Organismo son unidireccionales (RangoLegal, Organimos no tienen referencia en las normas, Publicacion es una enumeración).
RangoLegal
idRangoLegal (Integer), nomeG (String), nomeC (String). Los nombres de los atributos nomeG y nomeG no coinciden con los de la base de datos y tienen tamaño 128, además, son únicos. La clave primaria es auto numérica. Sin relaciones directas.
Organismo
idOrganismo (Integer), nome (String). El nombre es único. La clave primaria es autonumérica. Sin relaciones directas.
Publicacion (enumeración) y PublicacionConverter
Enumeración con 3 valores: DOG, BOE, DOCE, en este orden y atributos llamados idPublicacion, descripcion.
Implanta una clase PublicacionConverter para que el mapeo correcto de los valores de la enumeración en la columna idPublicacion:
idClasificacion (Integer), nomeG (String), nomeC (String). La clave primaria es autonumérica y los nombres de los atributos no coinciden con los de la base de datos, además, son únicos.
Relaciones:
Norma, pues hay una tabla intermedia ClasificacionNorma (fíjate que dicha entidad, ClasificacionNorma, no se implanta, por lo que es una relación muchos a muchos). La instanciación debe ser “perezosa” con todas las operaciones en cascada.
Norma
idNorma (Integer), publicacion (Publicacion, enumeración), numeroPublicacion (Integer), numeroPaxina (Integer), titulo (String), dataNorma (LocalDate), dataPublicacion (LocalDate), derogada (boolean). Ten en cuenta que el título es un texto largo (Clob) a la hora de mapear.
Relaciones con:
Organismo (unidireccional)
RangoLegal (unidireccional)
DocumentoNorma: el propietario de la relación es DocumentoNorma.
Clasificacion hay una tabla intermedia ClasificacionNorma (fíjate que dicha entidad, ClasificacionNorma, no se implanta y que una Norma puede tener muchas clasificaciones y viceversa). La instanciación debe ser “perezosa” con todas las operaciones en cascada.
Documento
idDocumento (Integer), mimeType (String), extension (String), titulo (String), titulo (String), documento (byte[]), tamanho (Integer), idioma (String). Especifica tamaño de los atributos de la tabla. Además, el documento es BLOB y con instanciación perezosa.
Relaciones con:
DocumentoNorma: el propietario de la relación es DocumentoNorma.
DocumentoNorma
idDocumentoNorma (IdDocumentoNorma), numero (Integer). Ten en cuenta que la clave es compuesta y debes crear una clase IdDocumentoNorma que representa a la clave compuesta.
Relaciones con:
Norma. Mapea la clave.
Documento. Mapea la clave.
3. DTO y Consultas
En la clase AppConsultas realiza las siguientes consultas en JPQL. Ten en cuenta que las consultas JPQL son mucho más sencillas y sólo incorporan la condición de JOIN, el ON, para entidades no relacionadas.
Liste las clasificaciones y la cantidad de normas que contienen (incluidos los que no tienen). Debe devolver en nombre de la clasificación (nombreG), el número de normas (puede ser 0) y el idClasificacion.
SELECTC.nome_g, Count(N.idNorma), C.idClasificacion FROM Norma AS N RIGHTJOIN (Clasificacion ASCLEFTJOIN ClasificacionNorma AS CN ONC.idClasificacion = CN.idClasificacion) ON N.idNorma = CN.idNorma GROUPBYC.nome_g, C.idClasificacion ORDERBY1;
Liste los rangos legales y la cantidad de normas que contienen. Debe devolver el nombre de RangoLegal (nomeG), la cantidad (puede ser 0) y el idRangoLegal.
SELECT R.nome_g, Count(N.idNorma), R.idRangoLegal FROM RangoLegal R LEFTJOIN Norma N ON R.idRangoLegal = N.idRangoLegal GROUPBY R.nome_g, R.idRangoLegal ORDERBY R.idRangoLegal ASC;
Dada la clase NormaDTO del proyecto, realiza una consulta que pida el idRango y muestre las normas, de tipo NormaDTO, con ese idRangoLegal (por ejemplo, idRangoLegal igual a 11).
La clase NormaDTO tiene los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.
En la Entidad Norma, crea dos consultas con nombre, llamadas Norma.findByTitulo y Norma.countByTitulo, que devuelvan las normas a partir de un título recogido por parámetro. Haz uso de ellas.
Crea una interface DAO<T, K >, que recoge el tipo de objeto, el tipo de la clave primaria:
import java.util.List;
publicinterfaceDAO<T, K>{
voidsave(T t);
voiddelete(T t);
T get(K k);
voidupdate(T t);
List<T>findAll();
List<T>findByTituloContaining(String titulo, int offset, int limit);
List<T>findByTituloContaining(String titulo);
intcountAll();
intcountByTitulo(String titulo);
}
Paginación de Normas
Se trata de realizar una aplicación que permita consultar las Normas de la base de datos por nombre (pide la introducción de un texto) y muestre las normas de la base de datos de 10 en 10. Se debe mostrar las NormasDTO.
Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.
La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.
Crea una clase NormaDTO que tenga los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.
Crea una clase NormaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.
7. NormaDAO y NormaRepository
NormaDAO implements DAO<Norma, Integer>
Implementación mediante patrón DAO de las operaciones con la entidad Norma.
Dispone de un atributo privado y final, de tipo EntityManager, em, para referenciar al gestor de entidades, y un constructor que recoge la el objeto de este tipo.
Implantación de los cuatro métodos de la interfaz:
T get(K k): devuelve la norma con esa clave.
List<T> findByCadenaContaining(String titulo): haciendo uso de la consulta con nombre Norma.findByTitulo consulta las normas que contienen ese título.
List<T> findByCadenaContaining(String titulo, int offset, int limit): haciendo uso de la consulta con nombre “Norma.findByTitulo” consulta las normas que contienen ese título y devuelve limit elementos empezando en la posición offset.
int countByTitulo(String titulo): haciendo uso de la consulta con nombre Norma.countByTitulo, devuelve el número de normas que contiene ese título.
Comprueba el funcionamiento en la clase AppConsultas.
NormaRepository
Repositorio de String Data JPA, que, además, contiene dos métodos más de los del JpaRepository:
Un método que devuelve la lista de Normas que contiene un título (como en el caso anterior).
Un método que devuelve el número de normas que contienen el título recogido.
Comprueba el funcionamiento dentro del, creando un método testData dentro de la clase LexislacionApplication.
La aplicación debe permitir realizar las siguientes operaciones:
Listar todas las normas.
Listar todas las normas que contienen un título.
Listar el número de normas que contienen un título.
6. Servicio Rest con Spring Boot Data JPA y paginación
Listar todas las normas que contienen un título, de 10 en 10.
Listar todas las normas que contienen un título, de 10 en 10, a partir de una página concreta.
Realiza las pruebas con Postman ;-)
Modifica la aplicación para que la paginación se realice con el método findAll de la interfaz PagingAndSortingRepository.
Paginación de Normas: modifica la aplicación para que permita consultar las Normas de la base de datos por nombre (pide la introducción de un texto) y muestre las normas de la base de datos de 10 en 10. Se debe mostrar las NormasDTO.
Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.
La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.
Crea una clase NormaDTO que tenga los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.
Crea una clase NormaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.
Usando CommandLineRunner: implementa la interfaz CommandLineRunner y sobreescribe el método run.
Ejemplo:
@SpringBootApplicationpublicclassMyApplicationimplements CommandLineRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Overridepublicvoidrun(String... args) {
// Aquí va el código de la aplicación }
}
Usando ApplicationRunner: implementa la interfaz ApplicationRunner y sobreescribe el método run.
Ejemplo:
@SpringBootApplicationpublicclassMyApplicationimplements ApplicationRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Overridepublicvoidrun(ApplicationArguments args) {
// Aquí va el código de la aplicación }
}
Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@ComponentpublicclassMiComponente {
@PostConstructpublicvoidinit() {
// Aquí va el código de la aplicación }
}
Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplicationpublicclassMyApplication {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Beanpublic CommandLineRunner run() {
return args -> {
// Aquí va el código de la aplicación };
}
}
Ejecutores de código al inicio de la Aplicación
Para la realización de una aplicación de consola en Spring Boot, es necesario crear un proyecto de Spring Boot y modificar la clase principal de la aplicación para que sea una aplicación de consola.
@SpringBootApplicationpublicclassMiAplicacionimplements ApplicationRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
}
@Overridepublicvoidrun(ApplicationArguments args) {
// Aquí va el código de la aplicación }
}
Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@ComponentpublicclassMiComponente {
@PostConstructpublicvoidinit() {
// Aquí va el código de la aplicación }
}
@Component es una anotación que marca una clase como un componente de Spring. Spring escaneará las clases anotadas con @Component y las registrará en el contexto de la aplicación.
@PostConstruct es una anotación que se utiliza en un método que debe ejecutarse después de que se haya completado la construcción de un bean. Spring ejecutará el método anotado con @PostConstruct después de que se haya creado el bean.
Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplicationpublicclassMiAplicacion {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
}
@Beanpublic CommandLineRunner run() {
return args -> {
// Aquí va el código de la aplicación };
}
}
@Bean es una anotación que marca un método como un productor de un bean administrado por Spring. Spring llamará al método anotado con @Bean para crear el bean y lo registrará en el contexto de la aplicación.
Diferencias entre ejecutores de código al inicio de la Aplicación
Estos ejecutores se utilizan para ejecutar la lógica al iniciar la aplicación:
ApplicationRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.
ApplicationRunnerrecoge ApplicationArguments, que tiene métodos como getOptionNames(), getOptionValues() y getSourceArgs().
CommandLineRunner también es una Interfaz Funcional con el método run.
CommandLineRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.
Acepta los argumentos como un array de String que se pasan en el momento del inicio del servidor.
Ambos proporcionan la misma funcionalidad y la única diferencia entre CommandLineRunner y ApplicationRunner es que CommandLineRunner.run() acepta un array de String[], mientras que ApplicationRunner.run() acepta ApplicationArguments como argumento.
SLF4J (The Simple Logging Facade for Java) es una fachada o interfaz para varios sistemas de registro de eventos (logging) en Java. Permite a los desarrolladores cambiar de sistema de registro de eventos en tiempo de ejecución sin tener que modificar el código fuente. Para más información, visita la página oficial de SLF4J: http://www.slf4j.org/
Gson es una biblioteca Java que se utiliza para convertir objetos Java en su representación JSON. También puede ser utilizado para convertir una cadena JSON en un objeto Java equivalente. Gson es una biblioteca de código abierto desarrollada por Google. Puedes encontrar más información en la página oficial de Gson: https://github.com/google/gson
Jackson es una biblioteca Java de código abierto para convertir objetos Java en su representación JSON y viceversa. Jackson es una de las bibliotecas de serialización y deserialización JSON más populares en Java. Puedes encontrar más información en la página oficial de Jackson: https://github.com/FasterXML/jackson
Jackson Core es una biblioteca Java de código abierto para procesar JSON (Stream API). Jackson Core proporciona las clases básicas para trabajar con JSON, como JsonNode, JsonParser y JsonGenerator. Puedes encontrar más información en la página oficial de Jackson: https://github.com/FasterXML/jackson-core
JUnit es un framework open-source que se utiliza para realizar pruebas unitarias en Java. JUnit es una herramienta importante en el desarrollo de software, ya que permite a los desarrolladores probar su código de manera eficiente y asegurarse de que funciona correctamente. Puedes encontrar más información en la página oficial de JUnit: https://junit.org/junit5/
Para trabajar con bases de datos, necesitamos los drivers JDBC correspondientes.
4.1. H2
H2 es una base de datos relacional escrita en Java. Es muy rápida, de código abierto y se puede ejecutar en modo embebido o en modo servidor. Además, admite transacciones, encriptación, funciones de usuario o procedimientos almacenados. Además, puede almacenarse en memoria o en disco.
Es importante hacer notar que las incompatibilidades entre versiones diferentes de H2, por lo que se recomienda tener control sobre qué versión se está utilizando.
URL: jdbc:h2:mem:testdb (base de datos en memoria)
Driver: org.h2.Driver
URL (fichero): jdbc:h2:rutaALaBaseDatos;DATABASE_TO_UPPER=false (base de datos en fichero)
El Driver JDBC para H2 hace la conversión automática de los nombres de las tablas y columnas a mayúsculas, por lo que si queremos conservar los nombres originales, debemos añadir DATABASE_TO_UPPER=false a la URL de conexión.
4.2. SQLite JDBC Driver
SQLite es una base de datos relacional embebida, que no requiere un servidor. Es muy ligera y rápida, y se puede utilizar en aplicaciones de escritorio, móviles o en la web. Puedes encontrar más información en la página oficial de SQLite: https://www.sqlite.org/index.html
Existen varias implementaciones de SQLite en Java, pero vamos a usar Xerial SQLite JDBC Driver:
URL: jdbc:sqlite:rutaALaBaseDatos (base de datos en fichero)
Driver: org.sqlite.JDBC
Existen otras API para SQLite, como las versiones originales de androidx: https://developer.android.com/jetpack/androidx/releases/sqlite, pero dicha versión no es compatible con Java SE y se usaba antiguamente para android, antes de la aparicion de Room.
4.3. PostgreSQL JDBC Driver
PostgreSQL es un sistema de gestión de bases de datos relacional de código abierto y muy potente. Puedes encontrar más información en la página oficial de PostgreSQL: https://www.postgresql.org/
MySQL Connector/J es un controlador JDBC Tipo 4, lo que significa que es una implementación Java pura del protocolo MySQL y no depende de las bibliotecas de cliente MySQL. Como los anteriores, este controlador admite el registro automático con DriverMaganer, lo que significa que no es necesario cargar explícitamente el controlador.
HSQLDB es una base de datos relacional escrita en Java. Es muy rápida, de código abierto y se puede ejecutar en modo embebido o en modo servidor: https://hsqldb.org/
URL: jdbc:hsqldb:mem:testdb (base de datos en memoria)
Driver: org.hsqldb.jdbc.JDBCDriver
URL para servidor: jdbc:hsqldb:hsql://localhost/testdb
URL para fichero: jdbc:hsqldb:file:nombrebasededatos
5. Dependencias para JPA
5.1. Jakarta Persistence API (JPA)
La Java Persistence API (JPA) es una especificación de Java que describe la gestión de la persistencia de los objetos en las aplicaciones Java. JPA define un conjunto de interfaces y anotaciones que permiten a los desarrolladores mapear objetos Java a tablas de bases de datos y viceversa. Puedes encontrar más información en la página oficial de JPA:
Hibernate es un framework de mapeo objeto-relacional (ORM) para Java. Hibernate simplifica el desarrollo de aplicaciones Java que interactúan con bases de datos relacionales. Puedes encontrar más información en la página oficial de Hibernate: https://hibernate.org/
Pronto se lanzará la versión final de Hibernate 7, que será compatible con JPA 3.2. Esperamos.
5.3. EclipseLink
EclipseLink es otro framework de mapeo objeto-relacional (ORM) para Java. EclipseLink es una implementación de la especificación JPA y proporciona una serie de características avanzadas, como el mapeo de herencia, el mapeo de tablas, el mapeo de relaciones y la consulta de objetos. Puedes encontrar más información en la página oficial de EclipseLink: https://www.eclipse.org/eclipselink/
Pronto se lanzará la versión final de EclipseLink 5, que será compatible con JPA 3.2. Esperamos.
6. Dependencias para Spring
6.1. Spring Core
Spring Core es el núcleo del framework Spring. Proporciona las funcionalidades básicas de Spring, como la inyección de dependencias y la gestión de transacciones. Puedes encontrar más información en la página oficial de Spring: https://spring.io/projects/spring-framework
Spring Boot es un proyecto de Spring que simplifica el desarrollo de aplicaciones Java. Proporciona una serie de características, como la configuración automática, el embebido de servidores, la gestión de dependencias y la creación de aplicaciones ejecutables. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot
Spring Boot Starter es una colección de dependencias que se utilizan comúnmente en las aplicaciones Spring Boot. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot
Spring Data JPA es un proyecto de Spring que simplifica el acceso a datos en aplicaciones Java. Proporciona una serie de características, como la creación de repositorios, la generación de consultas y la gestión de transacciones. Puedes encontrar más información en la página oficial de Spring Data JPA: https://spring.io/projects/spring-data-jpa
Spring Boot Starter Data JPA es una colección de dependencias que se utilizan comúnmente en las aplicaciones Spring Boot que utilizan Spring Data JPA. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot
Kotlin es un lenguaje de programación moderno y conciso que se ejecuta sobre la JVM. Kotlin es interoperable con Java, lo que significa que puedes utilizar las bibliotecas de Java en Kotlin y viceversa. Puedes encontrar más información en la página oficial de Kotlin: https://kotlinlang.org/
IDEs como IntelliJ IDEA o Android Studio soportan Kotlin de forma nativa, pero también puedes usar Kotlin en otros IDE añadiendo las dependencias necesarias.
Scala es un lenguaje de programación funcional y orientado a objetos que se ejecuta sobre la JVM. Scala es interoperable con Java, lo que significa que puedes utilizar las bibliotecas de Java en Scala y viceversa. Puedes encontrar más información en la página oficial de Scala: https://www.scala-lang.org/
IDEs como IntelliJ IDEA o Eclipse soportan Scala de forma nativa, pero también puedes usar Scala en otros IDE añadiendo las dependencias necesarias.
El Singleton es un patrón de diseño creacional que garantiza que una clase tenga solo una instancia, además, proporciona un punto de acceso global a esa instancia.
1. Necesidad del patrón
El patrón Singleton resuelve dos problemas al mismo tiempo, violando el Principio de Responsabilidad Única:
Garantizar que una clase tenga solo una única instancia.
¿Por qué alguien querría controlar cuántas instancias tiene una clase? La razón más común es controlar el acceso a un recurso compartido, como una base de datos o un archivo.
Imagina que creaste un objeto, pero después decides crear uno nuevo. En lugar de recibir un objeto fresco, obtendrás el que ya fue creado anteriormente.
Este comportamiento es imposible de implementar con un constructor normal, ya que una llamada al constructor siempre debe devolver un objeto nuevo por diseño.
Acceso global a un objeto.
Los clientes pueden no darse cuenta de que están trabajando con el mismo objeto todo el tiempo.
Proporciona un punto de acceso global a esa instancia. ¿Recuerdas esas variables globales que solíamos usar para almacenar objetos esenciales? Aunque son convenientes, también son inseguras, ya que cualquier código podría sobrescribirlas y provocar un fallo en la aplicación.
Al igual que una variable global, el patrón Singleton permite acceder a un objeto desde cualquier parte del programa. Sin embargo, también protege esa instancia para que no sea sobrescrita por otro código.
Además, no queremos que el código que resuelve el problema esté disperso por todo el programa. Es mejor mantenerlo en una sola clase, especialmente si el resto del código ya depende de ella.
Hoy en día, el patrón Singleton es tan popular que la gente puede referirse a algo como un Singleton incluso si solo resuelve uno de estos problemas.
2. Implementación
Todas las implementaciones de Singleton tienen en común los siguientes dos pasos:
Hacer que el constructor por defecto sea privado, para evitar que otros objetos usen el operador new con la clase Singleton.
Crear un método de creación estático que actúe como un constructor. Internamente, este método llama al constructor privado para crear un objeto y lo almacena en un campo estático, normalmente llamado instance. Todas las llamadas posteriores a este método devolverán el objeto almacenado en caché.
Si el código tiene acceso a la clase Singleton, entonces puede llamar a su método estático. Así, cada vez que se llame a ese método, siempre se devolverá el mismo objeto.
3. Ejemplos
El gobierno es un excelente ejemplo del patrón Singleton. Un país solo puede tener un único gobierno oficial. Independientemente de la identidad de las personas que lo conforman, el título “Gobierno de Galicia” es un punto de acceso global que identifica al grupo de personas a cargo.
Otro posible ejemplo es una clase de registro. Esta clase controla el acceso a un archivo de registro en el disco. Solo puede haber un archivo de registro en un sistema, y un solo objeto que controla el acceso a ese archivo.
En referencia a acceso a datos, un objeto de conexión a la base de datos también puede ser un Singleton. En este caso, el Singleton controla el acceso a un conjunto de conexiones a la base de datos subyacente. El que creemos una clase Singleton para esto no significa que solo podamos tener una conexión a la base de datos, sino que solo queremos una instancia de la clase que controla las conexiones.
4. Estructura
El Singleton declara un método estático getInstance que devuelve la misma instancia de la clase.
El constructor del Singleton debe estar oculto para el código cliente (private). Llamar al método getInstance debería ser la única forma de obtener el objeto Singleton.
5. Pseudocódigo
En este ejemplo, la clase de conexión a la base de datos actúa como un Singleton. Esta clase no tiene un constructor público, por lo que la única forma de obtener su objeto es llamando al método getInstance.
// La clase Database define el método `getInstance` que permite// a los clientes acceder a la misma instancia de una conexión// a la base de datos en todo el programa.classDatabase {
// Campo estático para almacenar la instancia única.privatestatic Database instance;
// El constructor es privado para evitar llamadas directas con `new`.privateDatabase() {
// Código de inicialización, como la conexión real a la base de datos. }
// Método estático que controla el acceso a la instancia del Singleton.publicstatic Database getInstance() {
if (instance ==null) {
synchronized (Database.class) {
if (instance ==null) {
instance =new Database();
}
}
}
return instance;
}
// Método de lógica de negocio para ejecutar consultas.publicvoidquery(String sql) {
// Aquí pueden ir restricciones o lógica de caché. }
}
// Uso del Singleton en la aplicación.classApplication {
publicstaticvoidmain(String[] args) {
Database foo = Database.getInstance();
foo.query("SELECT ...");
Database bar = Database.getInstance();
bar.query("SELECT ...");
// La variable `bar` contiene la misma instancia que `foo`. }
}
6. Aplicabilidad
Debería usarr el patrón Singleton cuando:
Una clase en el programa debe tener una única instancia accesible por todos los clientes, como un objeto de base de datos compartido.
Necesitas un control más estricto sobre las variables globales.
El patrón Singleton deshabilita todas las demás formas de crear objetos de una clase, excepto mediante un método especial de creación.
Es posible modificar esta restricción para permitir múltiples instancias si es necesario, simplemente cambiando el método getInstance.
7. Cómo Implementarlo
Agrega un campo estático privado (instance) en la clase para almacenar la instancia Singleton.
Declara un método estático público (getInstance) para obtener la instancia Singleton.
Implementa una “inicialización perezosa”, creando el objeto solo en la primera llamada y almacenándolo en el campo estático.
Haz el constructor privado, de modo que solo la clase Singleton pueda llamarlo.
Reemplaza todas las llamadas al constructor en el código cliente por llamadas al método estático de creación.
8. Ventajas y desventajas
✅ Ventajas:
Garantiza que una clase tenga solo una única instancia.
Proporciona un punto de acceso global a esa instancia.
Se inicializa solo cuando se solicita por primera vez.
❌ Desventajas:
Viola el Principio de Responsabilidad Única, ya que resuelve dos problemas al mismo tiempo.
Puede enmascarar un mal diseño si los componentes del programa dependen demasiado entre sí.
Requiere un manejo especial en entornos multihilo para evitar que múltiples hilos creen varias instancias.
Puede ser difícil de probar con pruebas unitarias, ya que los frameworks de prueba suelen basarse en la herencia y los métodos estáticos no pueden sobrescribirse en la mayoría de los lenguajes.
9. Relación con otros patrones
Una clase Facade puede convertirse en un Singleton si solo se necesita una única instancia.
Flyweight es similar a Singleton si se reduce todo el estado compartido a un solo objeto, pero:
Singleton solo tiene una instancia, mientras que Flyweight puede tener varias con diferentes estados internos.
Singleton puede ser mutable, pero Flyweight es inmutable.
Abstract Factory, Builder y Prototype pueden implementarse como Singletons.
Para la mayor parte de estos ejercicios trabajaremos con la base de datos PostgreSQL. La instalación de postgres y configuración inicial puedes consultarla en:
Antes de comenzar con los ejercicios, necesitamos conocer el patrón Singleton y cómo se puede implementar de forma Thread-Safe. Para ello, a modo de ejemplo, empezaremos creando una clase sencilla que implante el patrón Singleton y Thread-Safe, para después aplicarlo a la creación de una clase JPAUtil que disponga de un único EntityManagerFactory para todas las operaciones de persistencia.
Crea una clase NumeroGenerado que implemente el patrón Singleton y Thread-Safe. La clase debe disponer de un método generarNumero() que devuelva un número aleatorio entre 0 y 100. Al crear el objeto se generará un número aleatorio que se mantendrá durante toda la ejecución del programa.
Haz un programa que cree 10 hilos que generen 2 números aleatorios cada uno y los muestren por pantalla.
publicclassMain {
publicstaticvoidmain(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
NumeroGenerado numeroGenerado = NumeroGenerado.getInstance();
for (int j = 0; j < 2; j++) {
System.out.println(numeroGenerado.generarNumero());
}
}).start();
}
}
}
Singleton Generador de números aleatorios
Crea una clase Generador que implemente el patrón Singleton y Thread-Safe. La clase debe emplear para generar números aleatorios y disponer de un método generarNumero() que devuelva un número aleatorio entre 0 y 100.
Haz un programa que cree 10 hilos que generen 3 números aleatorios cada uno y los muestren por pantalla.
publicclassMain {
publicstaticvoidmain(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Generador generador = Generador.getInstance();
for (int j = 0; j < 3; j++) {
System.out.println(generador.generarNumero());
}
}).start();
}
}
}
Singleton contenedor de propiedades
Crea una clase Propiedades que implemente el patrón Singleton y Thread-Safe. La clase debe disponer de un método getProperty(String clave) que devuelva el valor de una propiedad almacenada en un Map<String, String>. Al crear el objeto se cargarán las propiedades de un fichero propiedades.properties que se encuentra en el directorio resources del proyecto.
Crea un fichero propiedades.properties en el directorio resources con las siguientes propiedades:
Para cargar un fichero de propiedades en un proyecto Java, puedes emplear la clase Properties de Java. Un ejemplo de cómo cargar un fichero de propiedades sería el siguiente:
Properties es una clase que hereda de Hashtable<Object, Object> y que se emplea para cargar propiedades de un fichero de texto. Las propiedades se cargan en un objeto de tipo Properties y se acceden a través de su método getProperty(String clave).
1. Creación de EntityManagerFactory con patrón Singleton y Thread-Safe
1.1. EntityManagerFactory Singleton: JPAUtil
Crea una clase JPAUtil que implemente el patrón Singleton y Thread-Safe. La clase debe tener las siguientes características:
La clase debe tener un atributo estático, JPAUtil instance, que sea único para toda la aplicación.
Un atributo de instancia de tipo EntityManagerFactory emf.
Un método getInstance que devuelva una instancia de JPAUtil.
Un método getEntityManagerFactory que devuelva una instancia de EntityManagerFactory, recogiendo el nombre de la unidad de persistencia. Ten en cuenta que, como es único para toda la aplicación, debe ser creado una única vez y reutilizado. El problema de esta implementación es que sólo se puede trabajar con una única unidad de persistencia.
Un método, isEntityManagerFactoryClosed, que devuelva si la factoría es nula o está cerrada.
Un método para cerrar la factoría.
Ten en cuenta que la clase EntityManagerFactory es costosa de crear y debe ser única para toda la aplicación. Por ello, debe ser creada una única vez y reutilizada.
1.2. EntityManagerFactory Singleton con propiedades
Añade a la clase anterior un método para que el EntityManagerFactory contenga un mapa de propiedades que se le pasan al método createEntityManagerFactory() de Persistence. El mapa de propiedades debe tener las siguientes propiedades:
jakarta.persistence.jdbc.url: la URL de la base de datos.
jakarta.persistence.jdbc.user: el usuario de la base de datos.
jakarta.persistence.jdbc.password: la contraseña de la base de datos.
jakarta.persistence.jdbc.driver: el driver de la base de datos.
jakarta.persistence.schema-generation.database.action: la acción de la base de datos.
jakarta.persistence.schema-generation.create-source: la fuente de creación de la base de datos.
1.3. EntityManagerFactory Singleton para cada unidad de persistencia
Mejora: el EntityManager debe ser creado con el método createEntityManager() de la factoría y debe ser único para cada unidad de persistencia.
Para ello, en vez de tener una única instancia de EntityManagerFactory, debes tener un Map de EntityManagerFactory, una para cada unidad de persistencia, en el que la clave sea el nombre de la unidad de persistencia y el valor un objeto de tipo EntityManagerFactory.
Solución EntityManagerFactoriesUtil
package com.javhoz.ad.suzukiviolin.entities;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import java.util.HashMap;
publicclassEntityManagerFactoriesUtil {
privatestatic EntityManagerFactoriesUtil instance;
privatefinal HashMap<String,EntityManagerFactory> entityManagerFactory;
privateEntityManagerFactoriesUtil(){
entityManagerFactory =new HashMap<>();
}
publicstatic EntityManagerFactoriesUtil getInstance(){
if(instance ==null){
synchronized (EntityManagerFactoriesUtil.class){
if(instance==null){
instance =new EntityManagerFactoriesUtil();
}
}
}
return instance;
}
/**
* Método que verifica si la unidad de persistencia está cerrada o no para evitar que se cree más de una vez
* la misma unidad de persistencia.
* @param unidadPersistencia
* @return
*/publicbooleanisEntityManagerFactoryClosed(String unidadPersistencia){
return!entityManagerFactory.containsKey(unidadPersistencia) ||!entityManagerFactory.get(unidadPersistencia).isOpen();
}
public EntityManagerFactory getEntityManagerFactory(String unidadPersistencia) {
// Del mismo modo que hacemos doble chequeo en el patrón Singleton, aquí también lo hacemos// para evitar que se cree más de una vez la misma unidad de persistencia. Si bien la clave// es el nombre de la unidad de persistencia, no se puede garantizar que no se cree más de una// vez la misma unidad de persistencia.if(isEntityManagerFactoryClosed(unidadPersistencia)) {
synchronized (EntityManagerFactoriesUtil.class) {
if (isEntityManagerFactoryClosed(unidadPersistencia)) {
entityManagerFactory.put(unidadPersistencia, Persistence.createEntityManagerFactory(unidadPersistencia));
}
}
}
return entityManagerFactory.get(unidadPersistencia);
}
public EntityManager getEntityManager(String unidadPersistencia){
if(!isEntityManagerFactoryClosed(unidadPersistencia))
return entityManagerFactory.get(unidadPersistencia).createEntityManager();
return getEntityManagerFactory(unidadPersistencia).createEntityManager();
}
}
2. Base de datos SuzukiViolin
Dada la base de datos SuzukiViolin, debes crear una base de datos en PostgreSQL con el nombre SuzukiViolinR en local, creando el archivo de persistencia para crear la base de datos, pues estaremos en modo de creación de esquema.
JPAUtil
Crea una clase JPAUtil Singleton y con doble comprobación que se conecte a la base de datos SuzukiViolinR y que tenga un método getEntityManagerFactory que devuelva un EntityManagerFactory para la unidad de persistencia SuzukiViolinR (resuelto en el ejercicio anterior).
Autor
Crea uns entidad Autor con idAutor (autonum), nombre (no nulo, tamaño 125), apellidos (no nulo, tamaño 255), además de codigoNacionalidad (columna nacionalidad, tamaño 3), fechaNacimiento (localDate), fechaDefuncion, bibliografía y generoMusical (de tipo MusicGenre).
Crea dos autores y añádelos a la base de datos. Puedes crear los autores que ya existen en la base de datos original.
Haz que la clave primaria de la tabla sea idAutor y se genere de manera automática. Hazlo de los distintos modos que conoces:
Con GenerationType.IDENTITY.
Con GenerationType.SEQUENCE.
Con la secuencia por defecto: ¿cómo se llama la secuencia por defecto? ¿Cuál es el valor inicial y el tamaño de asignación?
Con @SequenceGenerator: con el nombre idAutorSeq, secuencia AutorSeg, valor inicial 2 y tamaño de asignación 20.
Con GenerationType.TABLE:
Con la tabla por defecto: ¿cómo se llama la tabla por defecto?
Con @TableGenerator con los siguientes valores: generador idAutorGen, tabla SuzukiId, columna clave, columna valor, valor de la clave primaria Autor. haz que el valor inicial sea 2 y el tamaño de asignación 20.
Con GenerationType.UUID: debes cambiar el tipo de la clave primaria a UUID.
Enumeraciones
Las 3 enumeraciones que no debes modificar, pues ya están totalmente implementadas, incluso con métodos de utilidad si fuesen necesarios:
Tonality (Tonalidad o armadura en castellano): SOL_M,…
PlayListType: tipo de playlist (Repertorio, Libro, Concierto,…)
MusicGenre: género musical (barroco, clasicismo,..)
A) Tonalidad o armadura en castellano: SOL_M,… Haz que en la base de datos se guarde como una cadena de texto.
B) Debes realizar las siguientes clases de conversión de tipo, autoaplicable en el caso de género musical (para PlayListType y Tonality, deberás aplicarlas sobre las clases correspondientes):
MusicGenreConverter: convierte la enumeración de MusicGenre en una cadena para guardar en la base de datos. Debe guardar en la base de datos el nombre del género, pero con la primera letra en mayúsculas (para obtener el valor del nombre de una enumeración debe invocarse al método name()). Help: usa substring.
PlayListTypeConverter: convierte la enumeración en un Integer, con el IdTipoLista de la enumeración (existen métodos getIdTipoLista y fromId, para recoger el valor de la enumeración y el entero, respectivamente).
Relaciones
Crea las siguientes relaciones entre las entidades, tomando las consideraciones de acuerdo con la estructura de la base de datos (a excepción de Author con PiezaMusical, que difiere de la indicada en la base de datos original):
Author con PiezaMusical: un autor puede tener varias piezas musicales y una pieza puede tener varios autores. Inicialmente, haz que un autor pueda tener varias piezas y una pieza sólo pertenezca a un autor. Crea varias piezas y un autor e insértalo en la base de datos. Hazlo con y sin CASCADE.
Pieza con RecursoMusical. un recurso musical pertenece a una única pieza musical y una pieza musical puede tener varios recursos musicales.
Pieza con Partitura. Una pieza puede tener varias partituras.
Author con Foto: ImagenAutor es la tabla en la base de datos, pero teniendo en cuenta que Foto NO es una entidad y sólo tiene sentido si existe un autor.
Pieza con PlayList. Una Pieza puede formar parte de varias PlayList y viceversa. La clave primaria de la tabla intermedia es idPieza y idLista.
Instrumento con Partitura. Ten en cuenta que necesitamos el orden en el que aparece el instrumento en la partitura. Genera la clave compuesta con @IdClass y con @Id en las propiedades correspondientes.
Clase Dao
A) Crea una clase Dao, AuthorDao, que implemente los métodos de acceso a la base de datos. La clase debe tener los siguientes métodos:
saveAutor(Autor autor): guarda un autor en la base de datos.
updateAutor(Autor autor): actualiza un autor en la base de datos.
deleteAutor(Autor autor): elimina un autor de la base de datos.
getAutor(Long id): obtiene un autor de la base de datos.
getAllAutores(): obtiene todos los autores de la base de datos.
getAllAutoresByGeneroMusical(MusicGenre generoMusical): obtiene todos los autores de la base de datos por género musical.
getAllAutoresByNacionalidad(String nacionalidad): obtiene todos los autores de la base de datos por nacionalidad.
Crea un programa que cree un autor y lo guarde en la base de datos. Después, actualiza el autor y lo guarda de nuevo. Por último, elimina el autor de la base de datos.
B) PlayListDao: crea una clase PlayListDao que implemente los métodos de acceso a la base de datos. La clase debe tener los siguientes métodos:
savePlayList(PlayList playList): guarda una lista de reproducción en la base de datos.
updatePlayList(PlayList playList): actualiza una lista de reproducción en la base de datos.
deletePlayList(PlayList playList): elimina una lista de reproducción de la base de datos.
getPlayList(Long id): obtiene una lista de reproducción de la base de datos.
getAllPlayLists(): obtiene todas las listas de reproducción de la base de datos.
getAllPlayListsByTipo(PlayListType tipo): obtiene todas las listas de reproducción de la base de datos por tipo.
Crea un programa que cree una lista de reproducción y la guarde en la base de datos. Después, actualiza la lista de reproducción y la guarda de nuevo. Por último, elimina la lista de reproducción de la base de datos.
Para la mayor parte de estos ejercicios trabajaremos con la base de datos PostgreSQL. La instalación de postgres y configuración inicial puedes consultarla en:
Antes de comenzar con los ejercicios, necesitamos conocer el patrón Singleton y cómo se puede implementar de forma Thread-Safe. Para ello, a modo de ejemplo, empezaremos creando una clase sencilla que implante el patrón Singleton y Thread-Safe, para después aplicarlo a la creación de una clase JPAUtil que disponga de un único EntityManagerFactory para todas las operaciones de persistencia.
1. Creación de EntityManagerFactory con patrón Singleton y Thread-Safe
1.1. EntityManagerFactory Singleton: JPAUtil
Crea una clase JPAUtil que implemente el patrón Singleton y Thread-Safe. La clase debe tener las siguientes características:
La clase debe tener un atributo estático, JPAUtil instance, que sea único para toda la aplicación.
Un atributo de instancia de tipo EntityManagerFactory emf.
Un método getInstance que devuelva una instancia de JPAUtil.
Un método getEntityManagerFactory que devuelva una instancia de EntityManagerFactory, recogiendo el nombre de la unidad de persistencia. Ten en cuenta que, como es único para toda la aplicación, debe ser creado una única vez y reutilizado. El problema de esta implementación es que sólo se puede trabajar con una única unidad de persistencia.
Un método, isEntityManagerFactoryClosed, que devuelva si la factoría es nula o está cerrada.
Un método para cerrar la factoría.
Ten en cuenta que la clase EntityManagerFactory es costosa de crear y debe ser única para toda la aplicación. Por ello, debe ser creada una única vez y reutilizada.
1.2. EntityManagerFactory Singleton con propiedades
Añade a la clase anterior un método para que el EntityManagerFactory contenga un mapa de propiedades que se le pasan al método createEntityManagerFactory() de Persistence. El mapa de propiedades debe tener las siguientes propiedades:
jakarta.persistence.jdbc.url: la URL de la base de datos.
jakarta.persistence.jdbc.user: el usuario de la base de datos.
jakarta.persistence.jdbc.password: la contraseña de la base de datos.
jakarta.persistence.jdbc.driver: el driver de la base de datos.
jakarta.persistence.schema-generation.database.action: la acción de la base de datos.
jakarta.persistence.schema-generation.create-source: la fuente de creación de la base de datos.
1.3. EntityManagerFactory Singleton para cada unidad de persistencia
Mejora: el EntityManager debe ser creado con el método createEntityManager() de la factoría y debe ser único para cada unidad de persistencia.
Para ello, en vez de tener una única instancia de EntityManagerFactory, debes tener un Map de EntityManagerFactory, una para cada unidad de persistencia, en el que la clave sea el nombre de la unidad de persistencia y el valor un objeto de tipo EntityManagerFactory.
Solución EntityManagerFactoriesUtil
package com.javhoz.ad.suzukiviolin.entities;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import java.util.HashMap;
publicclassEntityManagerFactoriesUtil {
privatestatic EntityManagerFactoriesUtil instance;
privatefinal HashMap<String,EntityManagerFactory> entityManagerFactory;
privateEntityManagerFactoriesUtil(){
entityManagerFactory =new HashMap<>();
}
publicstatic EntityManagerFactoriesUtil getInstance(){
if(instance ==null){
synchronized (EntityManagerFactoriesUtil.class){
if(instance==null){
instance =new EntityManagerFactoriesUtil();
}
}
}
return instance;
}
/**
* Método que verifica si la unidad de persistencia está cerrada o no para evitar que se cree más de una vez
* la misma unidad de persistencia.
* @param unidadPersistencia
* @return
*/publicbooleanisEntityManagerFactoryClosed(String unidadPersistencia){
return!entityManagerFactory.containsKey(unidadPersistencia) ||!entityManagerFactory.get(unidadPersistencia).isOpen();
}
public EntityManagerFactory getEntityManagerFactory(String unidadPersistencia) {
// Del mismo modo que hacermos doble chequeo en el patrón Singleton, aquí también lo hacemos// para evitar que se cree más de una vez la misma unidad de persistencia. Si bien la clave// es el nombre de la unidad de persistencia, no se puede garantizar que no se cree más de una// vez la misma unidad de persistencia.if(isEntityManagerFactoryClosed(unidadPersistencia)) {
synchronized (EntityManagerFactoriesUtil.class) {
if (isEntityManagerFactoryClosed(unidadPersistencia)) {
entityManagerFactory.put(unidadPersistencia, Persistence.createEntityManagerFactory(unidadPersistencia));
}
}
}
return entityManagerFactory.get(unidadPersistencia);
}
public EntityManager getEntityManager(String unidadPersistencia){
if(!isEntityManagerFactoryClosed(unidadPersistencia))
return entityManagerFactory.get(unidadPersistencia).createEntityManager();
return getEntityManagerFactory(unidadPersistencia).createEntityManager();
}
}
2. Base de datos de legislación: Rango Legal y Organismo
Realizar un proyecto JPA con EclipseLink que mapee las tablas de la base de datos muestre todos los rangos legales y organismos de la base de datos.
RangoLegal:
idRangoLegal (Integer), nomeG (String), nomeC (String), descripcion (texto largo). Los nombres de los atributos nomeG y nomeG no coinciden con los de la base de datos y tienen tamaño 128, además, son únicos. La clave primaria es auto numérica.
Organismo:
idOrganismo (Integer), nome (String), descripcion (texto largo). El nombre es único. La clave primaria es autonumérica.
Crea una base de datos en PostgreSQL con el nombre Lexislacion y las tablas RangoLegal y Organismo.
Haz que la aplicación migre los datos de la base de datos de MariaDB a la de PostgreSQL.
3. Alquiler de películas
Crea una base de datos en PostgreSQL con el nombre videoclub y restaura la base de datos db-videoclub.tar.
Dicha base de datos tiene 15 tablas:
actor: almacena datos de actores, incluidos el nombre y el apellido.
film: almacena datos de películas como título, año de lanzamiento, duración, clasificación, etc.
film_actor: almacena las relaciones entre películas y actores.
category: almacena datos de las categorías de las películas.
film_category: almacena las relaciones entre películas y categorías.
store: contiene los datos de la tienda, incluidos el personal gerencial y la dirección.
inventory: almacena datos del inventario.
rental: almacena datos de alquiler.
payment: almacena los pagos de los clientes.
staff: almacena datos del personal.
customer: almacena datos de los clientes.
address: almacena datos de dirección para el personal y los clientes.
city: almacena los nombres de las ciudades.
country: almacena los nombres de los países.
Ahora que conocemos todo sobre nuestra base de datos de videoclub de ejemplo, pasemos a cargar la misma base de datos en el servidor de la base de datos PostgreSQL. Los pasos para ello se enumeran a continuación:
Paso 1: Cree una base de datos de videoclub, abriendo la consola SQL. Una vez que abra la consola, deberás añadir las credenciales necesarias para la base de datos, que se verían algo así:
Servidor [localhost]:
Base de datos [postgres]:
Puerto [5432]:
Nombre de usuario [postgres]:
Contraseña para el usuario postgres:
Ahora, usando la declaración CREATE DATABASE, cree una nueva base de datos de la siguiente manera:
CREATEDATABASE videoclub;
Paso 2: Cargue el archivo de la base de datos creando una carpeta en la ubicación deseada (por ejemplo, C:\users\sample_database\bd-videoclub.tar). Ahora abra el símbolo del sistema y navegue hasta la carpeta bin de la carpeta de instalación de PostgreSQL como se muestra a continuación (en el caso de haber añadido la ruta de instalación de PostgreSQL al PATH no será necesario navegar hasta la carpeta bin):
cd C:\ruta\a\la\carpeta\bin
Use la herramienta pg_restore para cargar datos en la base de datos videoclub que acabamos de crear mediante el siguiente comando:
De momento, mapea el campo fulltext como un String:
Haz que la entidad Pelicula tenga una con la entidad Idioma (un idioma puede tener muchas películas, pero una película sólo puede tener un idioma).
CategoriaPelicula: haz que la entidad Pelicula tenga una relación con la entidad Categoria (una película puede tener muchas categorías y una categoría puede tener muchas películas), para ello, crea una entidad CategoriaPelicula que mapee la tabla film_category, que dispone de las siguientes columnas: film_id (int4), category_id (int4), last_update (timestamp). IMPORTANTE: la clave primaria de la tabla film_category es compuesta por film_id y category_id.
PeliculaActor: haz que la entidad Pelicula tenga una relación con la entidad Actor (una película puede tener muchos actores y un actor pudo haber realizado muchas películas), para ello, crea una entidad PeliculaActor que mapee la tabla film_actor, que dispone de las siguientes columnas: actor_id (int4), film_id (int4), last_update (timestamp). La clave primaria de la tabla film_actor es compuesta por actor_id y film_id.
Ciudad: mapee la tabla city que dispone de las siguientes columnas: city_id (serial4), city (varchar(50)), country_id (int4), last_update (timestamp). Haz que la entidad Ciudad tenga una relación con la entidad Pais (una ciudad pertenece a un único país y un país puede tener muchas ciudades).
Direccion: mapee la tabla address que dispone de las siguientes columnas: address_id (serial4), address (varchar(50)), address2 (varchar(50)), district (varchar(20)), city_id (int4), postal_code (varchar(10)), phone (varchar(20), last_update (timestamp). Haz que la entidad Direccion tenga una relación con la entidad Ciudad (una dirección pertenece a una única ciudad y una ciudad puede tener muchas direcciones).
Empleado: mapee la tabla staff que dispone de las siguientes columnas: staff_id (serial4), first_name (varchar(45)), last_name (varchar(45)), address_id (int4), email (varchar(50)), store_id (int4), active (boolean), username (varchar(16)), password (varchar(40)), last_update (timestamp). Haz que la entidad Empleado tenga una relación con la entidad Direccion (un empleado tiene una dirección y una dirección puede pertenecer a muchos empleados) y con la entidad Tienda.
Tienda: mapee la tabla store que dispone de las siguientes columnas: store_id (serial4), manager_staff_id (int4), address_id (int4), last_update (timestamp). Haz que la entidad Tienda tenga una relación con la entidad Direccion (una tienda tiene una dirección y una dirección puede pertenecer a muchas tiendas).
Inventario: mapee la tabla inventory que dispone de las siguientes columnas: inventory_id (serial4), film_id (int4), store_id (int4), last_update (timestamp). Haz que la entidad Inventario tenga una relación con la entidad Pelicula (un inventario tiene una película y una película puede estar en muchos inventarios) y con la entidad Tienda (un inventario pertenece a una tienda y una tienda puede tener muchos inventarios).
Cliente: mapee la tabla customer que dispone de las siguientes columnas: customer_id (serial4), store_id (int4), first_name (varchar(45)), last_name (varchar(45)), email (varchar(50)), address_id (int4), activebool (boolean), create_date (date), last_update (timestamp), active (int4). Haz que la entidad Cliente tenga una relación con la entidad Tienda (un cliente pertenece a una tienda y una tienda puede tener muchos clientes) y con la entidad Direccion (un cliente tiene una dirección y una dirección puede pertenecer a muchos clientes).
Alquiler: mapee la tabla rental que dispone de las siguientes columnas: rental_id (serial4), rental_date (timestamp), inventory_id (int4), customer_id (int4), return_date (timestamp), staff_id (int4), last_update (timestamp). Haz que la entidad Alquiler tenga una relación con la entidad Inventario (un alquiler tiene un inventario y un inventario puede tener muchos alquileres), con la entidad Cliente (un alquiler tiene un cliente y un cliente puede tener muchos alquileres) y con la entidad Staff (un alquiler tiene un empleado y un empleado puede tener muchos alquileres).
Pago: mapee la tabla payment que dispone de las siguientes columnas: payment_id (serial4), customer_id (int4), staff_id (int4), rental_id (int4), amount (numeric(5,2)), payment_date (timestamp). Haz que la entidad Pago tenga una relación con la entidad Alquiler (un pago tiene un alquiler y un alquiler puede tener muchos pagos), con la entidad Cliente (un pago tiene un cliente y un cliente puede tener muchos pagos) y con la entidad Staff (un pago tiene un empleado y un empleado puede tener muchos pagos).
Diagrama de la base de datos:
4. Pedidos PostgreSQL
Data la estructura de datos de MariaDB se define en el script bd-pedidos.sql, crea un proyecto JPA con Hibernate que mapee las tablas de la base de datos en PostgreSQL (no crees la base de datos en PostgreSQL, simplemente mapea las tablas, tampoco lo hagas en MariaDB):
Crea un proyecto con JPA y Hibernate tenga las siguientes entidades:
Producto: nombre no nulo. La imagen como bytea.
Cliente: dni y nombre no nulo.
Pedido: fecha no nula.
LineaPedido: cantidad de tipo entero corto y no nula.
Haz que el producto tenga la imagen guardada en la base de datos, no como cadena, de tipo bytea.
Los pedidos deben estar ordenados por fecha y las líneas de pedido por cantidad.
Producto dispone de una colección de elementos con los comentarios del pedido.
LineaPedido debe mapearse como una colección de elementos. Comprueba el resultado y hazlo como entidad.
LineaPedido debe tener una colección de tags.
Las relaciones deben actualizarse y borrarse en cascada.
5. Pedidos
Para este ejercicio usaremos la base de datos MariaDB definida en el script bd-pedidos.sql.
Deberás crear un proyecto JPA con EclipseLink que mapee las tablas de la base de datos, empleando las entidades del ejercicio anterior, pero mapeadas con EclipseLink.
Crea en el mismo archivo de persistencia, una nueva unidad de persistencia que se conecte a la base de datos de MariaDB con EclipseLink.
Migra los datos de la base de datos de PostgreSQL a la de MariaDB, si es que no lo has hecho en el ejercicio anterior.
La aplicación debe permitir hacer lo siguiente:
Mostrar todos los productos de la base de datos.
Mostrar todos los pedidos de un cliente.
Añadir un pedido.
Borrar un pedido.
Para ello, crea una clase AppPedidos con un menú que permita realizar las operaciones anteriores y una clase DAO para cada entidad.
La clase genérica DAO<T, K > recoge el tipo de objeto, el tipo de la clave primaria, que tenga los métodos comunes a todas las entidades. La clase DAO genérica debe tener como atributo un EntityManager. La clase DAO debe tener los métodos necesarios para realizar las operaciones anteriores, así como un atributo de tipo EntityManager.
6. Ejercicios Herencia
Ejercicios centrados en la herencia en JPA y las distintas estrategias de mapeo (SINGLE_TABLE, JOINED, TABLE_PER_CLASS) usando PostgreSQL como base de datos.
Nivel Básico: Estrategia SINGLE_TABLE
Objetivo
Aprender a usar la estrategia SINGLE_TABLE y entender cómo JPA gestiona la herencia con una única tabla.
Enunciado
Crea una jerarquía de clases con la clase base Vehiculo:
id, marca, modelo
Dos clases hijas:
Coche: numPuertas
Moto: tipoManillar
Usa la estrategia SINGLE_TABLE en la entidad padre.
Configura persistence.xml para usar PostgreSQL (con hibernate.hbm2ddl.auto=update).
Inserta y consulta objetos de tipo Vehiculo, Coche y Moto.
Nivel Intermedio: Estrategia JOINED
Objetivo
Aplicar la estrategia JOINED para guardar entidades hijas en tablas separadas, mejorando la normalización.
Enunciado
Crea la clase base Empleado:
id, nombre, dni
Subclases:
Programador: lenguajePrincipal
Diseniador: herramientaPreferida
Usa JOINED como estrategia de herencia.
Realiza una consulta que devuelva todos los empleados con su información específica (usa TypedQuery<Empleado>).
Inserta instancias de ambas subclases y observa el resultado en PostgreSQL.
Nivel Avanzado: Estrategia TABLE_PER_CLASS
Objetivo
Experimentar con TABLE_PER_CLASS, ideal cuando las subclases son muy distintas entre sí.
Enunciado
Crea una clase base abstracta Documento:
id, titulo, fechaCreacion
Subclases:
Factura: importeTotal
Informe: responsable
Usa la estrategia TABLE_PER_CLASS.
Añade datos de distintos documentos y consulta desde la clase base.
Observa cómo se crean tablas separadas por subclase y cómo afecta a las consultas con @Inheritance.
Discriminadores
En los ejercicios de SINGLE_TABLE y JOINED, añade:
@DiscriminatorColumn(name ="tipo")
y comprueba cómo afecta a la persistencia y consultas.
Los nombres de las tablas son en CamelCase, así como los nombres de los atributos. Ten en cuenta que muchos atributos no coinciden con los de las entidades.
Las entidades/enumeraciones que debes implantar están en amarillo.
Publicacion no es una entidad, es una enumeración, de la aplicación, por lo que no debe ser implantada como entidad (el idPublicacion en Norma coindice con el índice de la enumeración más 1: DOG, BOE, DOCE).
1. JPAUtil
Clase que implanta el patrón Singleton con doble comprobación para obtener el objeto de tipo EntityManager.
Las relaciones de la entidad Norma con RangoLegal y Organismo son unidireccionales (RangoLegal, Organimos no tienen referencia en las normas, Publicacion es una enumeración).
RangoLegal
idRangoLegal (Integer), nomeG (String), nomeC (String). Los nombres de los atributos nomeG y nomeG no coinciden con los de la base de datos y tienen tamaño 128, además, son únicos. La clave primaria es auto numérica. Sin relaciones directas.
Organismo
idOrganismo (Integer), nome (String). El nombre es único. La clave primaria es autonumérica. Sin relaciones directas.
Publicacion (enumeración) y PublicacionConverter
Enumeración con 3 valores: DOG, BOE, DOCE, en este orden y atributos llamados idPublicacion, descripcion.
Implanta una clase PublicacionConverter para que el mapeo correcto de los valores de la enumeración en la columna idPublicacion:
idClasificacion (Integer), nomeG (String), nomeC (String). La clave primaria es autonumérica y los nombres de los atributos no coinciden con los de la base de datos, además, son únicos.
Relaciones:
Norma, pues hay una tabla intermedia ClasificacionNorma (fíjate que dicha entidad, ClasificacionNorma, no se implanta, por lo que es una relación muchos a muchos). La instanciación debe ser “perezosa” con todas las operaciones en cascada.
Norma
idNorma (Integer), publicacion (Publicacion, enumeración), numeroPublicacion (Integer), numeroPaxina (Integer), titulo (String), dataNorma (LocalDate), dataPublicacion (LocalDate), derogada (boolean). Ten en cuenta que el título es un texto largo (Clob) a la hora de mapear.
Relaciones con:
Organismo (unidireccional)
RangoLegal (unidireccional)
DocumentoNorma: el propietario de la relación es DocumentoNorma.
Clasificacion hay una tabla intermedia ClasificacionNorma (fíjate que dicha entidad, ClasificacionNorma, no se implanta y que una Norma puede tener muchas clasificaciones y viceversa). La instanciación debe ser “perezosa” con todas las operaciones en cascada.
Documento
idDocumento (Integer), mimeType (String), extension (String), titulo (String), titulo (String), documento (byte[]), tamanho (Integer), idioma (String). Especifica tamaño de los atributos de la tabla. Además, el documento es BLOB y con instanciación perezosa.
Relaciones con:
DocumentoNorma: el propietario de la relación es DocumentoNorma.
DocumentoNorma
idDocumentoNorma (IdDocumentoNorma), numero (Integer). Ten en cuenta que la clave es compuesta y debes crear una clase IdDocumentoNorma que representa a la clave compuesta.
Relaciones con:
Norma. Mapea la clave.
Documento. Mapea la clave.
3. DTO y Consultas
En la clase AppConsultas realiza las siguientes consultas en JPQL. Ten en cuenta que las consultas JPQL son mucho más sencillas y sólo incorporan la condición de JOIN, el ON, para entidades no relacionadas.
Liste las clasificaciones y la cantidad de normas que contienen (incluidos los que no tienen). Debe devolver en nombre de la clasificación (nombreG), el número de normas (puede ser 0) y el idClasificacion.
SELECTC.nome_g, Count(N.idNorma), C.idClasificacion FROM Norma AS N RIGHTJOIN (Clasificacion ASCLEFTJOIN ClasificacionNorma AS CN ONC.idClasificacion = CN.idClasificacion) ON N.idNorma = CN.idNorma GROUPBYC.nome_g, C.idClasificacion ORDERBY1;
Liste los rangos legales y la cantidad de normas que contienen. Debe devolver el nombre de RangoLegal (nomeG), la cantidad (puede ser 0) y el idRangoLegal.
SELECT R.nome_g, Count(N.idNorma), R.idRangoLegal FROM RangoLegal R LEFTJOIN Norma N ON R.idRangoLegal = N.idRangoLegal GROUPBY R.nome_g, R.idRangoLegal ORDERBY R.idRangoLegal ASC;
Dada la clase NormaDTO del proyecto, realiza una consulta que pida el idRango y muestre las normas, de tipo NormaDTO, con ese idRangoLegal (por ejemplo, idRangoLegal igual a 11).
La clase NormaDTO tiene los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.
En la Entidad Norma, crea dos consultas con nombre, llamadas Norma.findByTitulo y Norma.countByTitulo, que devuelvan las normas a partir de un título recogido por parámetro. Haz uso de ellas.
Crea una interface DAO<T, K >, que recoge el tipo de objeto, el tipo de la clave primaria:
import java.util.List;
publicinterfaceDAO<T, K>{
voidsave(T t);
voiddelete(T t);
T get(K k);
voidupdate(T t);
List<T>findAll();
List<T>findByTituloContaining(String titulo, int offset, int limit);
List<T>findByTituloContaining(String titulo);
intcountAll();
intcountByTitulo(String titulo);
}
Paginación de Normas
Se trata de realizar una aplicación que permita consultar las Normas de la base de datos por nombre (pide la introducción de un texto) y muestre las normas de la base de datos de 10 en 10. Se debe mostrar las NormasDTO.
Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.
La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.
Crea una clase NormaDTO que tenga los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.
Crea una clase NormaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.
4. NormaDAO y NormaRepository
NormaDAO implements DAO<Norma, Integer>
Implementación mediante patrón DAO de las operaciones con la entidad Norma.
Dispone de un atributo privado y final, de tipo EntityManager, em, para referenciar al gestor de entidades, y un constructor que recoge la el objeto de este tipo.
Implantación de los cuatro métodos de la interfaz:
T get(K k): devuelve la norma con esa clave.
List<T> findByCadenaContaining(String titulo): haciendo uso de la consulta con nombre Norma.findByTitulo consulta las normas que contienen ese título.
List<T> findByCadenaContaining(String titulo, int offset, int limit): haciendo uso de la consulta con nombre “Norma.findByTitulo” consulta las normas que contienen ese título y devuelve limit elementos empezando en la posición offset.
int countByTitulo(String titulo): haciendo uso de la consulta con nombre Norma.countByTitulo, devuelve el número de normas que contiene ese título.
Comprueba el funcionamiento en la clase AppConsultas.
NormaRepository
Repositorio de String Data JPA, que, además, contiene dos métodos más de los del JpaRepository:
Un método que devuelve la lista de Normas que contiene un título (como en el caso anterior).
Un método que devuelve el número de normas que contienen el título recogido.
Comprueba el funcionamiento dentro del, creando un método testData dentro de la clase LexislacionApplication.
La aplicación debe permitir realizar las siguientes operaciones:
Listar todas las normas.
Listar todas las normas que contienen un título.
Listar el número de normas que contienen un título.
6. Servicio Rest con Spring Boot Data JPA y paginación
Listar todas las normas que contienen un título, de 10 en 10.
Listar todas las normas que contienen un título, de 10 en 10, a partir de una página concreta.
Realiza las pruebas con Postman ;-)
Modifica la aplicación para que la paginación se realice con el método findAll de la interfaz PagingAndSortingRepository.
Paginación de Normas: modifica la aplicación para que permita consultar las Normas de la base de datos por nombre (pide la introducción de un texto) y muestre las normas de la base de datos de 10 en 10. Se debe mostrar las NormasDTO.
Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.
La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.
Crea una clase NormaDTO que tenga los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.
Crea una clase NormaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.
Usando CommandLineRunner: implementa la interfaz CommandLineRunner y sobreescribe el método run.
Ejemplo:
@SpringBootApplicationpublicclassMyApplicationimplements CommandLineRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Overridepublicvoidrun(String... args) {
// Aquí va el código de la aplicación }
}
Usando ApplicationRunner: implementa la interfaz ApplicationRunner y sobreescribe el método run.
Ejemplo:
@SpringBootApplicationpublicclassMyApplicationimplements ApplicationRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Overridepublicvoidrun(ApplicationArguments args) {
// Aquí va el código de la aplicación }
}
Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@ComponentpublicclassMiComponente {
@PostConstructpublicvoidinit() {
// Aquí va el código de la aplicación }
}
Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplicationpublicclassMyApplication {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Beanpublic CommandLineRunner run() {
return args -> {
// Aquí va el código de la aplicación };
}
}
Ejecutores de código al inicio de la Aplicación
Para la realización de una aplicación de consola en Spring Boot, es necesario crear un proyecto de Spring Boot y modificar la clase principal de la aplicación para que sea una aplicación de consola.
@SpringBootApplicationpublicclassMiAplicacionimplements ApplicationRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
}
@Overridepublicvoidrun(ApplicationArguments args) {
// Aquí va el código de la aplicación }
}
Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@ComponentpublicclassMiComponente {
@PostConstructpublicvoidinit() {
// Aquí va el código de la aplicación }
}
@Component es una anotación que marca una clase como un componente de Spring. Spring escaneará las clases anotadas con @Component y las registrará en el contexto de la aplicación.
@PostConstruct es una anotación que se utiliza en un método que debe ejecutarse después de que se haya completado la construcción de un bean. Spring ejecutará el método anotado con @PostConstruct después de que se haya creado el bean.
Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplicationpublicclassMiAplicacion {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
}
@Beanpublic CommandLineRunner run() {
return args -> {
// Aquí va el código de la aplicación };
}
}
@Bean es una anotación que marca un método como un productor de un bean administrado por Spring. Spring llamará al método anotado con @Bean para crear el bean y lo registrará en el contexto de la aplicación.
Diferencias entre ejecutores de código al inicio de la Aplicación
Estos ejecutores se utilizan para ejecutar la lógica al iniciar la aplicación:
ApplicationRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.
ApplicationRunnerrecoge ApplicationArguments, que tiene métodos como getOptionNames(), getOptionValues() y getSourceArgs().
CommandLineRunner también es una Interfaz Funcional con el método run.
CommandLineRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.
Acepta los argumentos como un array de String que se pasan en el momento del inicio del servidor.
Ambos proporcionan la misma funcionalidad y la única diferencia entre CommandLineRunner y ApplicationRunner es que CommandLineRunner.run() acepta un array de String[], mientras que ApplicationRunner.run() acepta ApplicationArguments como argumento.
El Framework Spring es un framework popular y ampliamente utilizado para construir aplicaciones web y empresariales.
A lo largo de los años, el Framework Spring ha crecido exponencialmente al abordar las necesidades de las aplicaciones empresariales modernas, como seguridad, soporte para almacenes de datos NoSQL, gestión de big data, procesamiento por lotes, integración con otros sistemas, y más. Junto con sus proyectos secundarios, Spring se ha convertido en una plataforma viable para construir aplicaciones empresariales.
El Framework Spring es muy flexible y proporciona múltiples formas de configurar los componentes de la aplicación. Con funciones avanzadas combinadas con diversas opciones de configuración, la configuración de aplicaciones Spring se vuelve compleja y propensa a errores. Así, el equipo de Spring creó Spring Boot para abordar la complejidad de la configuración a través de su potente mecanismo de AutoConfiguración.
Descripción general del Framework Spring
El Framework Spring se creó principalmente como un contenedor de inyección de dependencias, pero es mucho más que eso. Spring es famoso por varias razones:
El enfoque de inyección de dependencias de Spring fomenta la escritura de código fácil de comprobar.
Facilidad de uso y capacidades potentes de gestión de transacciones de base de datos.
Spring simplifica la integración con otros frameworks Java, como JPA/Hibernate ORM y JooQ (biblioteca de SQL para Java).
Framework Web MVC de última generación para construir aplicaciones web.
Junto con el Framework Spring, muchos otros subproyectos de Spring ayudan a construir aplicaciones que abordan las necesidades empresariales modernas:
Spring Data: simplifica el acceso a datos desde datos relacionales y NoSQL.
Spring Batch: framework potente para el procesamiento por lotes.
Spring Security: framework de seguridad para proteger aplicaciones.
Spring Cloud: conjunto de herramientas para que los desarrolladores implementen patrones comunes de sistemas distribuidos, como Descubrimiento de Servicios, Gestión de Configuración, Disyuntor de Circuitos y más.
Spring Integration: implementación de patrones de integración empresarial para facilitar la integración con otras aplicaciones empresariales mediante mensajería ligera y adaptadores declarativos.
Spring Web Services: framework para construir servicios web SOAP.
Spring HATEOAS: framework para construir servicios RESTful que siguen el principio de HATEOAS (Hypermedia as the Engine of Application State).
Spring Shell: framework para construir aplicaciones de línea de comandos.
Spring MVC: framework web MVC para construir aplicaciones web.
Existen muchos otros proyectos interesantes que abordan diversas necesidades modernas de desarrollo de aplicaciones. Para obtener más información:
Veremos el código generado y cómo ejecutar una aplicación.
2. ¿Qué es Spring Boot?
Spring Boot es un framework que ayuda a los desarrolladores a construir aplicaciones basadas en Spring de manera rápida y fácil.
El objetivo principal de Spring Boot es crear rápidamente aplicaciones basadas en Spring sin necesitar escribir la misma configuración una y otra vez. Las características clave de Spring Boot incluyen:
Starters de Spring Boot
Autoconfiguración de Spring Boot
Gestión elegante de la configuración
Spring Boot Actuator
Soporte fácil de contenedor de servlet integrado
2.1. Spring Boot starters (Iniciadores de Spring Boot)
Spring Boot ofrece muchos módulos iniciadores (starters) para comenzar rápidamente con muchas de las tecnologías comúnmente utilizadas, como Spring MVC, JPA, Thymeleaf (spring-boot-starter-thymeleaf), MongoDB, Spring Batch, Spring Security, Solr o ElasticSearch…. Estos starters están preconfigurados con las dependencias de bibliotecas más utilizadas, por lo que no es necesario buscar versiones de bibliotecas compatibles y configurarlas manualmente.
Por ejemplo, el módulo starter spring-boot-starter-data-jpa incluye todas las dependencias necesarias para usar Spring Data JPA y las dependencias de la biblioteca Hibernate, ya que Hibernate es la implementación JPA más comúnmente utilizada.
Spring Boot soluciona el problema de que las aplicaciones de Spring necesitan una configuración compleja al eliminar la necesidad de configurar manualmente la configuración básica.
Spring Boot adopta una vista con opción de la aplicación y configura varios componentes automáticamente registrando beans basados en múltiples criterios. Estos criterios pueden ser:
Disponibilidad de una clase específica en el classpath.
Presencia o ausencia de un bean de Spring.
Presencia de una propiedad del sistema.
Ausencia de un archivo de configuración.
Por ejemplo, supongamos que tienes la dependencia spring-webmvc en el classpath (o en el archivo pom.xml) y no has registrado explícitamente un bean DispatcherServlet en tu configuración de Spring. En este caso, Spring Boot asume que estás intentando construir una aplicación web basada en SpringMVC y automáticamente intenta registrar DispatcherServlet si aún no está registrado.
Si tienes controladores de base de datos integrados en el classpath, como H2 o HSQL, y no has configurado explícitamente un bean DataSource, Spring Boot registrará automáticamente un bean DataSource utilizando configuraciones de base de datos en memoria.
2.3. Gestión de la Configuración
Spring admite la externalización de propiedades configurables mediante la configuración @PropertySource. Spring Boot lleva esto aún más lejos utilizando valores predeterminados sensatos y una potente vinculación de propiedades de tipo seguro a propiedades de bean. Spring Boot admite tener archivos de configuración separados para perfiles diferentes sin requerir muchas configuraciones.
El archivo application.properties o application.yml es un archivo de configuración que se encuentra en el directorio src/main/resources y contiene propiedades de configuración para la aplicación. Spring Boot carga automáticamente este archivo de configuración y vincula las propiedades a los beans de Spring.
2.4. Spring Boot Actuator
Obtener los diversos detalles de una aplicación en ejecución en producción es crucial para muchas aplicaciones. Spring Boot Actuator proporciona una amplia variedad de características preparadas para producción sin requerir que los desarrolladores escriban mucho código. Algunas de las características del Actuator de Spring son:
Visualización de los detalles de configuración de los beans de la aplicación.
Visualización de los mapeos de URL de la aplicación, detalles del entorno y valores de los parámetros de configuración.
Visualización de las métricas registradas de las comprobaciones de salud.
2.5. Soporte Fácil de Contenedor de Servlet Integrado
Tradicionalmente, al construir aplicaciones web, se necesitan crear módulos de tipo WAR y luego implementarlos en servidores externos como Tomcat y WildFly. Pero con Spring Boot puedes crear un módulo de tipo JAR e incrustar el contenedor de servlet en la aplicación muy rápidamente para que sea una unidad de implementación independiente. Además, durante el desarrollo, puedes ejecutar rápidamente el módulo de tipo JAR de Spring Boot como una aplicación Java desde el IDE o la línea de comandos utilizando una herramienta de compilación como Maven o Gradle.
3. Primera Aplicación Spring Boot: Spring Initializr y una aplicación web sencilla
Hay muchas formas de crear una aplicación Spring Boot. La forma más sencilla es utilizar Spring Initializr en https://start.spring.io/, un generador de aplicaciones Spring Boot en línea.
En el ejemplo debéis crear una sencilla aplicación web Spring Boot que sirve una página HTML sencilla y ver varios aspectos de una aplicación típica de Spring Boot.
Selecciona “Maven Project" y una versión de Spring Boot 3.4.3 (elige la última versión estable).
Nota
Nota: La versión de Spring Boot puede cambiar con el tiempo. Asegúrate de seleccionar la última versión estable.
Además, las versiones SNAPSHOT pueden no ser estables y pueden contener errores. Son versiones que no se han lanzado oficialmente.
No hagas caso de la imagen, aparecerá una versión más reciente en el inizializador. Úsala como referencia.
Introduce los detalles del proyecto Maven (ajústalos a las necesidades de tu proyecto):
Group (grupo): local.sanclemente.ad
Artifact (artefacto): springboot-basic
Nombre: springboot-basic
Nombre del paquete: local.sanclemente.ad.demo
Empaquetado: JAR
Versión de Java: 21 (o la versión que prefieras)
Lenguaje: Java
Haz clic en el botón "Add Dependencies”. Puedes buscar los starters si ya estás familiarizado con sus nombres. Verás muchos módulos organizados en varias categorías, como Core, Web, Template engines, AI, NOSQL o SQL. Selecciona la casilla de verificación “Web” desde la categoría Web.
Apacerecerá una lista de dependencias, selecciona “Web”:
Spring Web Web
Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
Como puedes ver, la dependencia spring-boot-starter-web se selecciona automáticamente. Esta dependencia es necesaria para crear aplicaciones web basadas en Spring MVC e incluye por defecto:
RestFul: servicios REST.
Spring MVC: para crear aplicaciones web.
Tomcat: como contenedor de servlet integrado.
Importante: versiones de dependencias
Spring Initializr selecciona automáticamente las versiones de las dependencias. Si deseas usar una versión específica de una dependencia, puedes especificarla manualmente en el archivo pom.xml.
Además, ten en cuenta que no siempre la última versión de Spring Boot es compatible con todas las dependencias. Si tienes problemas de compatibilidad, puedes probar con una versión anterior de Spring Boot, pues no permitirá seleccionar las dependencias que no sean compatibles.
Haz clic en el botón Generate para descargar el archivo ZIP con el proyecto Spring Boot.
Ahora puedes extraer el archivo ZIP descargado e importarlo en el IDEIntelliJ IDEA, Eclipse o NetBeans.
3.2. Uso de Spring Tool Suite
Spring Tool Suite (STS: https://spring.io/tools) es una extensión para IDEs ampliamente utilizados como Visual Studio Code, Eclipse o Theia y cuenta con muchos complementos relacionados con el framework Spring.
Desde la variante Eclipse de STS, selecciona File ➤ New ➤ Other ➤ Spring Boot ➤ Spring Starter Project ➤ Next.
Introduce los detalles del proyecto y haz clic en “Finish”.
Puedes crear un proyecto Spring Boot desde IntelliJ IDEA de la siguiente manera:
Selecciona File ➤ New ➤ Project ➤ Spring Boot ➤ Next.
Introduce los detalles del proyecto y haz clic en “Next”.
Selecciona los starters y haz clic en “Next”.
Finalmente, introduce el nombre del proyecto y haz clic en “Finish”.
Nota
El soporte para Spring Framework solo viene con la edición comercial IntelliJ IDEA Ultimate, no con la edición gratuita Community. Si deseas usar la edición Community de IntelliJ IDEA, puedes generar el proyecto mediante Spring Initializr e importarlo en IntelliJ IDEA como un proyecto Maven/Gradle.
3.4. Usando NetBeans IDE
NetBeans IDE no hay soporte integrado para crear proyectos de Spring Boot en NetBeans, pero la comunidad ha construido el complemento NB Spring Boot (consultad: https://github.com/AlexFalappa/nbspringboot), que permite crear aplicaciones de Spring Boot directamente desde el IDE.
El pom.xml incluye el módulo: spring-bootstarter-parent. Lo primero que debemos tener en cuenta aquí es que el módulo Maven springboot-basic hereda del módulo spring-boot-starter-parent. Al heredar de spring-bootstarter-parent, este nuevo módulo tendrá automáticamente los siguientes beneficios:
Solo necesitas especificar la versión de Spring Boot una vez en la configuración del módulo principal. No es necesario especificar la versión para todas las dependencias de inicio y otras bibliotecas de soporte. Para ver la lista de bibliotecas de soporte, consulta el archivo pom.xml del módulo Maven org.springframework.boot:springboot-dependencies:{version}
El módulo principal spring-boot-starter-parent ya incluye los plugins más comúnmente utilizados, como maven-jar-plugin, maven-surefire-plugin, maven-war-plugin, exec-maven-plugin y maven-resources-plugin, con configuraciones predeterminadas sensatas.
Además de los plugins mencionados anteriormente, el módulo spring-bootstarter-parent también configura el spring-boot-maven-plugin, que se utiliza para construir JAR “fat” (que permite empaquetar todas las dependencias de la aplicación en un solo archivo JAR) y ejecutar la aplicación Spring Boot.
Por ejemplo:
<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>local.sanclemente.ad</groupId><artifactId>springboot-basic</artifactId><version>0.0.1-SNAPSHOT</version><!-- Inherit from Spring Boot Starter Parent. Módulo principal de Spring Boot --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.3</version></parent><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><version>42.7.3</version></dependency><dependency><groupId>org.mariadb.jdbc</groupId><artifactId>mariadb-java-client</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
Veremos más adelante el spring-boot-maven-plugin.
Por ejemplo selecciona solo el starter web (spring-boot-starter-web), pero el starter de prueba (spring-boot-starter-test) también se incluye de forma predeterminada. Se seleccionó la versión 17 o 21 de Java, de ahí que la propiedad <java.version>21</java.version> esté incluida. Este valor de java.version se utilizará para configurar la versión de JDK para el compilador Maven en el módulo spring-boot-starter-parent.
El módulo Spring Boot tipo JAR generado tendrá una clase Java de punto de entrada de la aplicación llamada SpringbootBasicApplication.java (el proyecto fue nombrado springboot-basic). Esta clase es una clase de configuración de Spring Boot anotada con @SpringBootApplication y tiene un método public static void main(String[] args), que puedes usar para ejecutar la aplicación.
Aquí, la clase SpringbootBasicApplication está anotada con la anotación @SpringBootApplication, que es una anotación compuesta. Como puedes ver en el código fuente de la anotación @SpringBootApplication, es una combinación de varias anotaciones: @SpringBootConfiguration, @EnableAutoConfiguration o @ComponentScan.
@Configuration indica que esta clase es una clase de configuración de Spring.
@ComponentScan habilita la exploración de componentes para beans de Spring en el paquete definido por la clase actual.
@EnableAutoConfiguration activa los mecanismos de autoconfiguración de Spring Boot.
Estás iniciando la aplicación llamando a SpringApplication.run(SpringbootBasicApplication.class, args) en el método main(). Puedes pasar una o más clases de configuración de Spring dentro del método SpringApplication.run(). Pero supongamos que tienes tu clase de punto de entrada de la aplicación en un paquete raíz. En ese caso, es suficiente pasar solo la clase de entrada de la aplicación, que se encarga de escanear otras clases de configuración de Spring en todos los subpaquetes.
4.3. La clase del controladora
Ahora debes crea un controlador sencillo de SpringMVC, llamado HomeController.java, como se muestra:
La clase anterior es un controlador sencillo de SpringMVC con un método controlador de solicitudes para la URL /, que devuelve la vista llamada index.html.
4.4. La vista HTML
Crea una vista HTML llamada index.html.
Spring Boot sirve el contenido estático desde los directorios src/main/resources/static/. Entonces, crea index.html en src/main/resources/static, como se muestra en:
Ahora, desde el IDE, ejecuta el método main() de SpringbootBasicApplication como una clase Java independiente que iniciará el servidor Tomcat integrado en el puerto 8080 y apunta el navegador a http://localhost:8080/. Deberías poder ver la respuesta: “Ola mundo!!”
También puedes ejecutar la aplicación Spring Boot usando spring-boot-maven-plugin, de la siguiente manera:
mvn spring-boot:run
/*
Clase principal Application.java en el paquete raíz.
*/package com.micompanhia.miproyecto;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublicclassApplication {
publicstaticvoidmain(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Es muy recomendable colocar la clase principal de entrada en el paquete raíz, por ejemplo, en com.micompanhia.miproyecto, de modo que las anotaciones @EnableAutoConfiguration y @ComponentScanescaneen automáticamente los beans de Spring, entidades JPA, y similares en el paquete raíz y todos sus subpaquetes.
Si tienes una clase de punto de entrada en un paquete anidado, es posible que necesites especificar explícitamente los basePackages para escanear los componentes de Spring, como se muestra:
/**
* Clase principal Application.java en un paquete NO raíz.
*/package com.micompanhia.miproyecto.config;
@Configuration@EnableAutoConfiguration@ComponentScan(basePackages ="com.micompanhia.miproyecto")
@EntityScan(basePackageClasses = Person.class)
publicclassApplication {
publicstaticvoidmain(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Aquí, la clase principal Application.java está en el paquete com.micompanhia.miproyecto.config, que no es el paquete raíz. Por lo tanto, se necesita especificar @ComponentScan(basePackages = "com.micompanhia.miproyecto") para que Spring Boot escanee com.micompanhia.miproyecto y todos sus subpaquetes en busca de componentes de Spring.
Además, debes ==indicar @EntityScan(basePackageClasses=Persona.class) para que Spring Boot escanee entidades JPA bajo el paquete donde existe Persona.class=0.
4.5. Fat JAR usando el complemento Maven Spring Boot
Puedes ejecutar tu aplicación directamente desde el IDE o usar maven spring-boot:run durante el desarrollo, pero finalmente necesitas crear una unidad de implementación que pueda ejecutarse en el entorno de producción sin ningún soporte del IDE. Puedes usar spring-boot-maven-plugin para crear una única unidad de implementación (un Fat JAR) ejecutando los siguientes objetivos de Maven:
mvn clean package
En el directorio target hay dos archivos importantes: springboot-basic-1.0-SNAPSHOT.jar y springboot-basic-1.0-SNAPSHOT.jar.original:
El archivo springboot-basic-1.0-SNAPSHOT.jar.original contiene solo las clases compiladas y los recursos de classpath.
Pero el contenido de springboot-basic-1.0-SNAPSHOT.jar es el siguiente:
Las clases compiladas de tu código fuente en src/main/java y los recursos estáticos de src/main/resources estarán en el directorio BOOT-INF/classes.
Todas las JAR dependientes en el directorio BOOT-INF/lib.
Clases en el paquete org.springframework.boot.loader que realizan la magia de Spring Boot para ejecutar la aplicación Spring Boot.
Puedes crear unidades de implementación autocontenidas para módulos de tipo JAR usando complementos como maven-shade-plugin, que empaqueta todas las clases de JAR dependientes en un solo archivo JAR. Pero Spring Boot sigue un enfoque diferente y te permite anidar JAR directamente dentro de tu archivo JAR de aplicación Spring Boot. Puedes obtener más información en http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#executable-jar.
Puedes ejecutar la aplicación con el siguiente comando:
Estructura del Proyecto Creado Usando Spring Initializr
La estructura de carpetas es similar a cualquier proyecto Maven o Gradle, pero hay pequeños cambios:
mvnw y mvnw.cmd son scripts que actúan como un envoltorio si no tienes Maven instalado en tu máquina.
pom.xml contiene todas las especificaciones que Maven necesita para construir el proyecto, como dependencias, plugins y repositorios.
BibliotecaApplication es la clase principal de la aplicación.
ApiBibliotecaApplicationTests es una clase de prueba sencilla que verifica que la aplicación se ejecute correctamente.
application.properties es un archivo que inicialmente está vacío pero ofrece la posibilidad de cargar todas las configuraciones relacionadas con la aplicación. También puedes crear un archivo con el mismo nombre pero con la extensión YML para configurar la aplicación.
static es un directorio donde puedes colocar archivos estáticos como CSS, JavaScript, imágenes y otros recursos.
templates es un directorio donde puedes colocar plantillas de vista como HTML, Thymeleaf, FreeMarker y otros.
Si lo prefieres, como application.properties está vacío, se puede eliminar ese archivo y crear un nuevo archivo, application.yml, con el contenido del Listado siguiente, que define los endpoints del actuador que la aplicación expone, el puerto y la URL predeterminada. La razón de hacer esto es reducir la complejidad del archivo de configuración para entender la jerarquía de propiedades, pero puedes hacer lo mismo con application.properties sin problemas.
management:
endpoints:
web:
base-path: /exposure:
include: "*"# Indica que todos los endpoints están expuestos.server:
port: 8080# Indica el puerto predeterminado de la aplicaciónservlet:
context-path: /api/biblioteca# Indica la URL predeterminada
Configuración con la URL Predeterminada y los Endpoints Exponenciales.
5.2. Ejecución de la Aplicación
Hay dos formas de ejecutar la aplicación: usar el IDE o la línea de comandos, que es la opción siguiente:
./mvnw spring-boot:run
Al ejecutar una salida similar al listado siguiente, con toda la información sobre la ubicación de la aplicación que se inicia, el servidor contenedor que utiliza la aplicación y otros detalles adicionales. La información más relevante de esta salida es el puerto y la URL predeterminada (/api/biblioteca), que siempre debes verificar en caso de que algo pueda estar mal.
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | ||(_| | ))))' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0-M4)
2024-04-15T11:05:12.370-03:00 INFO 1172745 --- [ restartedMain]
com.javhoz.biblioteca.ApiBibliotecaApplication : Starting
ApiBibliotecaApplication using Java 17.0.4 on pepecalo with PID 1172734
(/home/pepecalo/Codigo/api-biblioteca/target/classes started by pepecalo
in /home/pepecalo/Codigo/api-biblioteca )
2024-04-19T11:05:12.375-03:00 INFO 1172745 --- [ restartedMain]
com.javhoz.biblioteca.ApiBibliotrecaApplication : No active profile set,
falling back to 1 default profile: "default"
2024-04-19T11:05:12.425-03:00 INFO 1172745 --- [ restartedMain]
.e.DevToolsPropertyDefaultsPostProcessor : Devtools property
defaults active! Set 'spring.devtools.add-properties' to 'false'to disable
2022-09-19T11:05:12.426-03:00 INFO 1172745 --- [ restartedMain]
5.3. Ejecutar la aplicación desde consola
Para la realización de una aplicación de consola en Spring Boot, es necesario crear un proyecto de Spring Boot y modificar la clase principal de la aplicación para que sea una aplicación de consola.
@SpringBootApplicationpublicclassMiAplicacionimplements ApplicationRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
}
@Overridepublicvoidrun(ApplicationArguments args) {
// Aquí va el código de la aplicación }
}
Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@ComponentpublicclassMyComponent {
@PostConstructpublicvoidinit() {
// Aquí va el código de la aplicación }
}
@Component es una anotación que marca una clase como un componente de Spring. Spring escaneará las clases anotadas con @Component y las registrará en el contexto de la aplicación.
@PostConstruct es una anotación que se utiliza en un método que debe ejecutarse después de que se haya completado la construcción de un bean. Spring ejecutará el método anotado con @PostConstruct después de que se haya creado el bean.
Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplicationpublicclassMiAplicacion {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
}
@Beanpublic CommandLineRunner run() {
return args -> {
// Aquí va el código de la aplicación };
}
}
@Bean es una anotación que marca un método como un productor de un bean administrado por Spring. Spring llamará al método anotado con @Bean para crear el bean y lo registrará en el contexto de la aplicación.
Diferencias entre ejecutores de código al inicio de la Aplicación
Estos ejecutores se utilizan para ejecutar la lógica al iniciar la aplicación:
ApplicationRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.
ApplicationRunnerrecoge ApplicationArguments, que tiene métodos como getOptionNames(), getOptionValues() y getSourceArgs().
CommandLineRunner también es una Interfaz Funcional con el método run.
CommandLineRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.
Acepta los argumentos como un array de String que se pasan en el momento del inicio del servidor.
Ambos proporcionan la misma funcionalidad y la única diferencia entre CommandLineRunner y ApplicationRunner es que CommandLineRunner.run() acepta un array de String[], mientras que ApplicationRunner.run() acepta ApplicationArguments como argumento.
Como hemos estudiado, JPA (API de Persistencia de Jakarta, anteriormente conocida como Java Persistence API) es una especificación que define una API encargada de gestionar la persistencia de objetos y mapeos objeto-relacional.
Hibernate es la implementación más popular de esta especificación. Esto es, JPA establece qué hacer para persistir objetos, mientras que Hibernate indica cómo hacerlo.
Características JPA
La especificación JPA define lo siguiente:
Un mecanismo para especificar metadatos de mapeo: cómo se relacionan las clases persistentes y sus propiedades con el esquema de la base de datos. JPA se basa en gran medida en anotaciones de Java en las clases del modelo de dominio, aunque también se pueden escribir mapeos en archivos XML.
APIs para realizar operaciones CRUD básicas en instancias de clases persistentes, principalmente [jakarta.persistence.EntityManager](https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/entitymanager) para almacenar y cargar datos.
Un lenguaje y APIs para especificar consultas que se refieren a clases y propiedades de clases. Este lenguaje es el Jakarta Persistence Query Language (JPQL) y se asemeja a SQL. La API Criteria permite la creación programática de consultas de criterios sin manipulación de cadenas.
Cómo el motor de persistencia interactúa con instancias transaccionales para realizar comprobaciones de cambios, recuperación de asociaciones y otras funciones de optimización. La especificación JPA cubre algunas estrategias básicas de almacenamiento en caché.
Hibernate implementa JPA y respalda todos los mapeos, consultas y interfaces de programación estandarizados.
Ventajas de Hibernate
Productividad: Hibernate elimina gran parte del trabajo repetitivo y permite concentrarse en el problema comercial. Ya sea que prefieras una estrategia de desarrollo de aplicaciones de arriba hacia abajo, comenzando con un modelo de dominio, o de abajo hacia arriba, comenzando con un esquema de base de datos existente, Hibernate, utilizado junto con las herramientas adecuadas, reducirá significativamente el tiempo de desarrollo.
Mantenimiento: el ORM automatizada con Hibernate reduce las líneas de código, lo que hace que el sistema sea más comprensible y fácil de refactorizar. Hibernate proporciona un buffer entre el modelo de dominio y el esquema SQL, aislando cada modelo de cambios menores en el otro.
Rendimiento: aunque la persistencia codificada a mano puede ser más rápida en el mismo sentido en que el código ensamblador puede ser más rápido que el código Java, las soluciones automatizadas como Hibernate permiten el uso de muchas optimizaciones en todo momento. Un ejemplo es el almacenamiento en caché eficiente y fácilmente ajustable en la capa de aplicación, lo que permite a los desarrolladores optimizar los pocos cuellos de botella reales en lugar de optimizar prematuramente todo.
Independencia del proveedor: Hibernate puede ayudar a mitigar algunos de los riesgos asociados con el bloqueo del proveedor. Incluso si no está previsto cambiar de Sistema Gestor de Base de Datos, las herramientas ORM que admiten varios SGBDs brindan un cierto nivel de portabilidad. Además, la independencia del SGBS (DBMS en inglés) es útil en escenarios de desarrollo donde los ingenieros utilizan una base de datos local ligera pero implementan para pruebas y producción en un sistema diferente.
0.2. Spring Data
Spring Data es una familia de proyectos pertenecientes al framework Spring cuyo propósito es simplificar el acceso tanto a bases de datos relacionales como a bases de datos NoSQL.
Proporciona los elementos fundamentales del framework Spring que poseen todos los módulos de Spring Data.
Spring Data JPA es un subproyecto de Spring Data que simplifica el acceso a datos JPA. Spring Data JPAelimina la necesidad de escribir consultas SQL. En su lugar, puedes definir consultas personalizadas en el repositorio y Spring Data JPA generará las consultas SQL por ti.
Spring Data Commons:Spring Data Commons, parte del proyecto paraguas Spring Data, proporciona un modelo de metadatos para persistir clases Java e interfaces de repositorio independientes de la tecnología. Spring Data Commons, como parte de Spring Data, proporciona los elementos fundamentales del framework Spring que poseen todos los módulos de Spring Data.
// Ejemplo de interfaz de repositorio Spring Data CommonspublicinterfaceRepositorioComun<T, ID>extends Repository<T, ID> {
// Métodos de repositorio común Optional<T>findById(ID id);
T save(T entity);
// Otros métodos...}
Spring Data JPA:Spring Data JPA se encarga de la implementación de repositorios basados en JPA. Ofrece un soporte mejorado para capas de acceso a datos basadas en JPA al reducir el código repetitivo y crear implementaciones para las interfaces de repositorio. Es una capa adicional sobre las implementaciones JPA (como Hibernate). No solo puede utilizar todas las capacidades de JPA, sino que también agrega sus propias funciones, como la creación de consultas de base de datos generadas a partir de nombres de métodos.
// Ejemplo de Entidad JPA:@EntitypublicclassEntidad {
@Id@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String nombre;
// Otros atributos...// Getters y setters...}
// Ejemplo de interfaz de repositorio Spring Data JPApublicinterfaceRepositorioJPAextends JpaRepository<Entidad, Long> {
// Métodos de repositorio JPA List<Entidad>findByNombre(String nombre);
// Otros métodos persomalozados si es necesario...}
Spring Data JDBC:Spring Data JDBC se ocupa de la implementación de repositorios basados en JDBC. Ofrece un soporte mejorado para capas de acceso a datos basadas en JDBC. No ofrece una serie de capacidades de JPA, como el almacenamiento en caché o la carga diferida, lo que resulta en un ORM más simple y limitado.
// Ejemplo de Entidad JDBC:publicclassEntidad {
private Long id;
private String nombre;
// Otros atributos...// Getters y setters...}
// Ejemplo de interfaz de repositorio Spring Data JDBCpublicinterfaceRepositorioJDBCextends CrudRepository<Entidad, Long> {
// Métodos de repositorio JDBC List<Entidad>findByNombre(String nombre);
// Otros métodos persomalozados si es necesario...}
Spring Data REST:Spring Data REST se encarga de exportar repositorios de Spring Data como recursos RESTful:
// Ejemplo de interfaz de repositorio Spring Data REST @RepositoryRestResource(collectionResourceRel ="entidades", path ="entidades")
publicinterfaceRepositorioRESTextends PagingAndSortingRepository<Entidad, Long> {
// Métodos de repositorio REST List<Entidad>findByNombre(String nombre);
// Otros métodos persomalozados si es necesario... }
Spring Data MongoDB:Spring Data MongoDB se ocupa del acceso a la base de datos de documentos MongoDB. Se basa en la capa de acceso a datos de estilo repositorio y en el modelo de programación POJO:
// Ejemplo de Entidad MongoDB:@Document(collection ="entidadesMongo")
publicclassEntidadMongo {
@Idprivate String id;
private String nombre;
// Otros atributos...// Getters y setters...}
// Ejemplo de interfaz de repositorio Spring Data MongoDBpublicinterfaceRepositorioMongoextends MongoRepository<EntidadMongo, String> {
// Métodos de repositorio MongoDB List<EntidadMongo>findByNombre(String nombre);
// Otros métodos persomalozados si es necesario...}
Spring Data Redis:Spring Data Redis se ocupa del acceso a la base de datos clave-valor Redis. Se basa en liberar al desarrollador de gestionar la infraestructura y proporciona abstracciones de alto y bajo nivel para acceder al almacén de datos.
// Ejemplo de Entidad Redis:@RedisHash("entidadesRedis")
publicclassEntidadRedis {
@Idprivate String id;
private String nombre;
// Otros atributos...// Getters y setters...}
// Ejemplo de interfaz de repositorio Spring Data RedispublicinterfaceRepositorioRedisextends CrudRepository<EntidadRedis, String> {
// Métodos de repositorio Redis List<EntidadRedis>findByNombre(String nombre);
// Otros métodos persomalozados si es necesario...}
Spring Data Neo4j:Spring Data Neo4j se ocupa del acceso a la base de datos de grafos Neo4j. Se basa en liberar al desarrollador de gestionar la infraestructura y proporciona abstracciones de alto y bajo nivel para acceder al almacén de datos.
Spring LDAP:Spring LDAP se ocupa del acceso a los servicios de directorio LDAP.
El código fuente de Spring Data (junto con otros proyectos de Spring) se puede descargar libremente desde https://github.com/spring-projects.
Para aprovechar Hibernate de manera eficaz, es necesario poder ver e interpretar las declaraciones SQL que emite y comprender sus implicaciones de rendimiento. Para beneficiarse de las ventajas de Spring Data, es esencial anticipar cómo se crean el código estándar y las consultas generadas.
Spring Data hace que la implementación de la capa de persistencia sea aún más eficiente.
Spring Data JPA, uno de los proyectos de la familia, se encuentra sobre la capa JPA.
Spring Data JDBC, otro proyecto de la familia, se encuentra sobre JDBC.
Ventajas de Spring Data
Infraestructura compartida:Spring Data Commons, parte del proyecto Spring Data, proporciona un modelo de metadatos para persistir clases Java e interfaces de repositorio independientes de la tecnología. Ofrece sus capacidades a otros proyectos de Spring Data.
Eliminación de implementaciones DAO: Las implementaciones JPA utilizan el patrón DAO (Data Access Object). Este patrón comienza con la idea de una interfaz abstracta a una base de datos y asigna las llamadas de la aplicación a la capa de persistencia, ocultando los detalles de la base de datos. Spring Data JPA permite eliminar por completo las implementaciones DAO, lo que reduce la longitud del código.
Creación automática de clases
Utilizando Spring Data JPA, una interfaz DAO debe extender la interfaz Repository específica de JPA, JpaRepository. Spring Data JPA creará automáticamente una implementación para esta interfaz, eliminando la necesidad de que el programador se ocupe de esto.
Implementaciones predeterminadas para métodos: Spring Data JPA generará implementaciones predeterminadas para cada método definido por las interfaces de repositorio. Las operaciones básicas CRUD ya no necesitan implementarse, lo que reduce el código innecesario, acelera el desarrollo y elimina la posibilidad de introducir errores.
Consultas generadas: Puedes definir un método en tu interfaz de repositorio siguiendo un patrón de nombres. No es necesario escribir tus consultas manualmente, Spring Data JPA analizará el nombre del método y creará una consulta para ello.
Cercano a la base de datos si es necesario:Spring Data JDBC puede comunicarse directamente con la base de datos y evitar la “capa” de Spring Data JPA. Permite interactuar con la base de datos a través de JDBC, pero elimina el código innecesario utilizando las facilidades del framework Spring.
1. Repositorios de Spring Data
Spring Data JPA proporciona una interfaz de repositorio que extiende la interface ListCrudRepository<T,ID> y de ListPagingAndSortingRepository<T,ID>. CrudRepository (y su versión con listas) proporciona métodos CRUD básicos, métodos para paginación. Además, Spring Data JPA proporciona métodos adicionales para realizar consultas personalizadas.
Spring Data JPA también proporciona métodos para paginación y ordenación. Spring Data JPAgenerará consultas SQL para estos métodos.
Spring Data JPA proporciona soporte para capas de acceso a datos basadas en JPA al reducir el código repetitivo y crear implementaciones para las interfaces de repositorio.
Solo necesitamos definir nuestra propia interfaz de repositorio, extendiendo una de las interfaces de Spring Data.
El conjunto de interfaces de repositorio de Spring Data comunes es el siguiente:
Repository<T,ID>: es la interfaz base de Spring Data, precisamos heredarla para crear un repositorio y personalizarlo. No tiene métodos específicos declarados, pues es interface principal para definir un repositorio. El propósito general es gestionar el tipo de información (tipos) y ser capar de descubrir interfaces que la heredan durante el escaneo (en el classpath) para facilitar la creación de Beans de Spring.
CrudRepository<T,ID>: hereda Repository, añade métodos CRUD e implanta métodos para contar, borrar, comprobar existencia, recuperar y guardar entidades:
long count(): cuenta el número de entidades disponibles.
void delete(T entity): elimina una entidad.
void deleteAll(): borra todas las entidades gestionadas por el repositorio.
void deleteAll(Iterable<? extends T> entities): elimina todas las entidades de la colección.
void deleteById(ID id): elimina la entidad con el identificador especificado.
boolean existsById(ID id): comprueba si existe una entidad con el identificador especificado.
Iterable<T> findAll(): recupera todas las entidades.
Iterable<T> findAllById(Iterable<ID> ids): recupera todas las entidades con los ids especificados.
Optional<T> findById(ID id): recupera la entidad con el identificador recogido como parámetro.
<S extends T> save(S entity): guarda una entidad.
<S extends T> Iterable<S> saveAll(Iterable<S> entities): guarda todas las entidades del iterable.
ListCrudRepository<T,ID>: hereda CrudRepository y sobrescribe los métodos de CrudRepository para devolver listas en lugar de Iterable:
List<T> findAll(): recupera todas las entidades.
List<T> findAllById(Iterable<ID> ids): recupera todas las entidades con los ids especificados.
<S extends T> List<S> saveAll(Iterable<S> entities): guarda las entidades de la colección.
PagingAndSortingRepository: hereda Repository y declara métodos de búsqueda para paginación y ordenación. En la mayoría de los casos se combina con CrudRepository o similar (no JpaRepository, que ya hereda de PagingAndSortingRepository) para añadir funcionalidad de paginación y ordenación a los métodos CRUD:
Iterable<T> findAll(Sort sort): recupera todas las entidades ordenadas según las opciones de ordenación.
Page<T> findAll(Pageable pageable): recupera las entidades con las opciones de paginación del objeto Pageable.
Ejemplo de uso de PagingAndSortingRepository:
publicinterfaceRepositorioPaginacionextends PagingAndSortingRepository<Entidad, Long>, CrudRepository<Entidad, Long>{
// Métodos de repositorio con paginación y ordenación List<Entidad>findByNombre(String nombre, Pageable pageable);
// Otros métodos persomalozados si es necesario...}
Por heredar de PagingAndSortingRepository, el repositorio RepositorioPaginaciontendrá acceso a los métodos de paginación y ordenación: findAll(Sort sort) y findAll(Pageable pageable).
ListPagingAndSortingRepository<T,ID>: hereda de PagingAndSortingRepository<T,ID> (y por ello de Repository<T,ID>) y sobrescribe el método findAll(Sort sort) de PagingAndSortingRepository para devolver una lista en lugar de Iterable:
List<T> findAll(Sort sort): recupera todas las entidades ordenadas según las opciones de ordenación.
Interfaces específicas de JPA:
JpaRepository<T,ID>: hereda de hereda de ListCrudRepository<T,ID>, ListPagingAndSortingRepository<T,ID>, QueryByExampleExecutor<T> (y, por ello de CrudRepository<T,ID>, PagingAndSortingRepository<T,ID>, Repository<T,ID>) y agrega métodos específicos de JPA:
<S extends T> List<S> findAll(Example<S> example): recupera todas las entidades que coinciden con el ejemplo (subespecificación de QueryByExampleExecutor<T>).
<S extends T> List<S> findAll(Example<S> example, Sort sort): recupera todas las entidades que coinciden con el ejemplo y las ordena según las opciones de ordenación (subespecificación de QueryByExampleExecutor<T>).
getReferenceById(ID id): devuelve una referencia a la entidad con el identificador especificado.
getById(ID id): desaprobado por getReferenceById(ID id).
getOne(ID id): desaprobado por getReferenceById(ID id).
<S extends T> S saveAndFlush(S entity): guarda la entidad y la fuerza a la base de datos.
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities): guarda todas las entidades y las fuerza a la base de datos.
void flush(): fuerza todas las entidades gestionadas a la base de datos.
void deleteInBatch(Iterable<T> entities): desaprobado por deleteAllInBatch(Iterable<T> entities).
void deleteAllInBatch(Iterable<T> entities): elimina todas las entidades de la colección en una única consulta.
void deleteAllInBatch(): elimina todas las entidades en una única consulta.
void deleteAllByIdInBatch(Iterable<ID> ids): elimina todas las entidades con los ids especificados en una única consulta.
Para JDBC es preciso heredar de CrudRepository y agregar métodos específicos de JDBC.
Otros repositorios de Spring Data son:
MongoRepository<T,ID>: hereda ListCrudRepository<T,ID>, ListPagingAndSortingRepository<T,ID>, QueryByExampleExecutor<T> (y, por ello, de CrudRepository<T,ID>, PagingAndSortingRepository<T,ID> y Repository<T,ID>). Es el repositorio específico para trabajar con MongoDB.
Neo4jRepository<T,ID>: hereda de PagingAndSortingRepository<T,ID>, CrudRepository<T,ID> y QueryByExampleExecutor<T> (y, por ello, de Repository<T,ID>), es un repositorio específico para Neo4j.
Redis: no existe un RedisRepository específico en Spring Data. En su lugar, se puede utilizar el repositorio CrudRepository para interactuar con Redis, configurando adecuadamente la conexión a Redis en el application.properties o application.yml:
// Ejemplo de interfaz de repositorio Spring Data RedispublicinterfaceRepositorioRedisextends CrudRepository<EntidadRedis, String> {
// Métodos de repositorio Redis List<EntidadRedis>findByNombre(String nombre);
// Otros métodos personalizados si es necesario...}
LdapRepository<T>: hereda de ListCrudRepository<T,Name> (y por ello de CrudRepository<T,Name> y Repository<T,Name>), es específico para LDAP, con métodos: Optional<T> findOne(org.springframework.ldap.query.LdapQuery ldapQuery) y List<T> findAll(org.springframework.ldap.query.LdapQuery ldapQuery).
CassandraRepository: hereda de ListCrudRepository<T,ID> (y por ello de: CrudRepository<T,ID> y Repository<T,ID>). Permite la especificación de un tipo para la identidad de @Table (o @Persistable). Los repositorios Cassandra pueden definir tanto una única clave primaria, usar una clase de clave primaria o una clave primaria compuesta sin una clase de clave primaria. Los tipos que utilizan una clave primaria compuesta sin una clase de clave primaria deben usar MapId para declarar el valor de su clave.
SolrRepository: hereda de directamente de Repository<T,ID> y es una excelente opción para integrar Spring Data con el popular motor de búsqueda Solr.
Además, se precisa un archivo solr-named-queries.properties en el directorio resources con las consultas personalizadas:
Producto.findByName=name:?0
ElasticsearchRepository<T,ID>: hereda de PagingAndSortingRepository<T,ID>, CrudRepository<T,ID> (y, por ello, de Repository<T,ID>). Añade métodos específicos de Elasticsearch.
CouchbaseRepository<T,ID>: hereda de PagingAndSortingRepository<T,ID>, CrudRepository<T,ID> (y, por ello, de Repository<T,ID>). Añade métodos específicos de Couchbase.
GemfireRepository: hereda CrudRepository<T,ID> (y, por ello, de Repository<T,ID>) y agrega métodos específicos de Apache Geode.
La interfaz MensajeRepositorio extiende CrudRepository<Mensaje, Long>. Esto significa que es un repositorio de entidades Mensaje con un identificador Long. Recordemos que la clase Mensaje tiene un campo id anotado como @Id de tipo Long. Podemos llamar directamente a métodos como save, findAll o findById, heredados de CrudRepository, y podemos usarlos sin ninguna información adicional para ejecutar operaciones habituales contra una base de datos. Spring Data JPA creará una clase proxy que implementa la interfaz MensajeRepositorio e implementará sus métodos:
Ahora, guardemos un Mensaje en la base de datos usando Spring Data JPA: HolaMundoSpringDataJPA.java:
@ExtendWith(SpringExtension.class) // #A@ContextConfiguration(classes = {SpringDataConfiguration.class}) // #BpublicclassHolaMundoSpringDataJPA {
@Autowired// #Cprivate MensajeRepositorio repositorioMensaje; // #C@TestpublicvoidsaveMensaje() {
Mensaje message =new Mensaje(); // #D message.setText("Hola MUndo desde Spring Data JPA!"); // #D repositorioMensaje.save(message); // #E List<Mensaje> mensajes = (List<Mensaje>)repositorioMensaje.findAll(); // #F assertAll( // #G () -> assertEquals(1, mensajes.size()), // #G () -> assertEquals("Hola MUndo desde Spring Data JPA!", mensajes.get(0).getText()) // #H );
}
}
/*
#A Extendemos la prueba utilizando SpringExtension. Esta extensión se utiliza para integrar el contexto de prueba de Spring con JUnit 5 Jupiter: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/context/junit/jupiter/SpringExtension.html.
#B La configuración del contexto de prueba de Spring se realiza utilizando los beans definidos en la clase SpringDataConfiguration.
#C Se inyecta un bean MensajeRepositorio mediante "auto-cableado" de Spring. Esto es posible gracias a que el paquete com.javhoz.ad.repositories, donde se encuentra MensajeRepositorio, se usó como argumento en la anotación @EnableJpaRepositories. Si llamamos a repositorioMensaje.getClass(), veremos algo como com.sun.proxy.$Proxy41, un proxy generado por Spring Data.
#D Creamos una nueva instancia de la clase de modelo de dominio mapeada Mensaje y establecemos su propiedad de texto.
#E Persistimos el objeto mensaje. El método save se hereda de la interfaz CrudRepository y su cuerpo será generado por Spring Data JPA cuando se cree la clase proxy. Simplemente guardará una entidad Mensaje en la base de datos.
#F Recuperamos los mensajes del repositorio. El método findAll se hereda de la interfaz CrudRepository y su cuerpo será generado por Spring Data JPA cuando se cree la clase proxy. Simplemente devolverá todas las entidades pertenecientes a la clase Mensaje.
#G Verificamos el tamaño de la lista de mensajes recuperados de la base de datos y que el mensaje que persistimos está en la base de datos #H. Utilizamos el método assertAll de JUnit 5, que siempre verifica todas las afirmaciones que se le pasan, incluso si algunas de ellas fallan. Las dos afirmaciones que verificamos están conceptualmente relacionadas.
*/
La prueba de Spring Data JPA es considerablemente más corta que las que utilizan JPA o Hibernate nativo. Esto se debe a que se ha eliminado el código redundante, no hay más creación explícita de objetos ni control explícito de las transacciones. El objeto del repositorio se inyecta y proporciona los métodos generados de la clase proxy. La carga es más pesada ahora en el lado de la configuración, pero esto debería hacerse solo una vez por aplicación (que normalmente se ejecuta en un servidor de aplicaciones y no he puesto en el ejemplo).
1.2. Añadiendo Métodos a la Interfaz a un repositorio en Spring Data JPA
Supongamos una clase de modelo de dominio Usuario:
JpaRepositoryextiende PagingAndSortingRepository yCrudRepository. En realidad hereda de las subclases: ListCrudRepository que devuelven List en lugar de Iterable y ListPagingAndSortingRepository que devuelven List en lugar de Iterable y añaden paginación y ordenación. JpaRepository también agrega métodos específicos de JPA.
CrudRepository proporciona funcionalidad básica de CRUD.
PagingAndSortingRepository ofrece métodos convenientes para ordenar y paginar los registros
JpaRepository ofrece métodos relacionados con JPA, como la limpieza del contexto de persistencia y la eliminación de registros por lotes. Además, JpaRepository sobrescribe algunos métodos de CrudRepository, como findAll, findAllById o saveAll, para devolver List en lugar de Iterable.
También agregaremos una serie de métodos de consulta a la interfaz RepositorioUsuario:
El propósito de los métodos de consulta es recuperar información de la base de datos.
Spring Data JPA proporciona un mecanismo de construcción de consultas que creará el comportamiento de los métodos del repositorio según sus nombres. Analizaremos más adelante las consultas de modificación; ahora nos sumergiremos en las consultas cuyo propósito es encontrar información.
Este mecanismo de consulta elimina los prefijos y sufijos como find...By, get...By, query...By, read...By, count...By del nombre del método y analiza el resto.
Los nombres de los métodos deben seguir las reglas para determinar la consulta resultante. Si el nombre del método es incorrecto (por ejemplo, la propiedad de la entidad no coincide en el método de consulta), recibirás un error al cargar el contexto de la aplicación.
La tabla siguiente describe las palabras clave que Spring Data JPA admite y cómo se transpone cada nombre de método en JPQL.
Palabra clave
Ejemplo
Fragmento JPQL
Distinct
findDistinctByLastnameAndFirstname
select distinct … where x.lastname = ?1 and x.firstname = ?2
… where x.firstname like ?1 (parameter bound with appended %)
EndingWith
findByFirstnameEndingWith
… where x.firstname like ?1 (parameter bound with prepended %)
Containing
findByFirstnameContaining
… where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy
findByAgeOrderByLastnameDesc
… where x.age = ?1 order by x.lastname desc
Not
findByLastnameNot
… where x.lastname <> ?1
In
findByAgeIn(Collection ages)
… where x.age in ?1
NotIn
findByAgeNotIn(Collection ages)
… where x.age not in ?1
True
findByActiveTrue()
… where x.active = true
False
findByActiveFalse()
… where x.active = false
IgnoreCase
findByFirstnameIgnoreCase
… where UPPER(x.firstname) = UPPER(?1)
Se pueden declarar métodos que contengan expresiones como Distinct para establecer una cláusula DISTINCT, operadores como LessThan, GreaterThan, Between y Like o condiciones compuestas con And u Or.
Se pueden aplicar un orden estático con la cláusula OrderBy en el nombre del método de consulta haciendo referencia a una propiedad y proporcionando una dirección de ordenamiento (Asc o Desc).
Se puede usar IgnoreCase para propiedades que admitan dicha cláusula.
Para eliminar filas, debes reemplazar find con delete en los nombres de los métodos.
Además, Spring Data JPA examinará el tipo de retorno del método. Si deseas encontrar un Usuario y devolverlo en un contenedor Optional, el tipo de retorno del método será Optional<Usuario>. Se puede encontrar una lista completa de tipos de retorno posibles, junto con explicaciones detalladas en: https://docs.spring.io/spring-data/jpa/reference/repositories/query-return-types-reference.html.
2. Fichero de configuración application.properties
El fichero de configuración de Spring Boot se llama application.properties y es el lugar donde se definen las propiedades de la aplicación. Spring Boot busca automáticamente un archivo application.properties en el directorio src/main/resources. Si no se encuentra, se utilizarán las propiedades por defecto.
Por ejemplo, para configurar una base de datos PostgreSQL, el archivo application.properties podría ser:
spring.application.name=Nombre de la aplicación#spring.jpa.hibernate.ddl-auto=nonespring.datasource.url=jdbc:postgresql://localhost:5432/mibasededatosspring.datasource.username=usuariospring.datasource.password=contraseñaspring.datasource.driver-class-name=org.postgresql.Driverspring.jpa.show-sql=falsespring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImplspring.jpa.properties.hibernate.globally_quoted_identifiers=true#logging.level.org.hibernate.SQL=DEBUG#logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Estrategia de nombres
Hibernate utiliza nombres de campo mediante una estrategia física y una estrategia implícita.
Y Spring Boot proporciona valores por defecto para ambos:
spring.jpa.hibernate.naming.physical-strategy es por defecto to org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy.
spring.jpa.hibernate.naming.implicit-strategy vale por defecto org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
Podemos sobrescribir estos valores, pero de forma predeterminada, Spring Boot utiliza las estrategias de nombres de Hibernate:
Reemplaza puntos por guiones bajos.
Cambia CamelCase a minúsculas y separa las palabras con guiones bajos (snake_case).
Nombres de las tablas en minúsculas.
Identificadores entre comillas
Como SQL es un lenguaje declarativo, las palabras reservadas de la gramática son para uso intenterno y no pueden ser empleadas cuando declramos identificadores de la base de datos (catalogos, esquemas, tablas, columnas, nombres…)
Podemos hacer un escape manual de las palabras reservadas con comillas dobles, pues Hibernate no lo hace por defecto:
@Entity@Table(name ="\"User\"")
publicclassUser {
@Id@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String email;
// Getters y setters...}
Para que Hibernate utilice comillas dobles, debemos configurar la propiedad hibernate.globally_quoted_identifiers a true.
También puede escaparse con el carácter específico de hibernate:
@Entity@Table(name ="`User`")
publicclassUser {
@Id@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String email;
// Getters y setters...}
La tercera opción es poniendo la opción hibernate.globally_quoted_identifiers a true en el fichero application.properties:. De esta forma Hibernate escapará todos los identificadores con comillas dobles.
Para poder usar identificadores entre comillas en las consultas de Spring Data JPA, debemos configurar la estrategia de nombre físico de Hibernate a org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl.
Hazlo con la estrategia de herencia de JPA de JOINED.
Crea una nueva clase Vehiculo que se llame VehiculoDTO que tenga los campos:
idVehiculo.
marca
modelo
año
precio
propietario.
Crea las entidades JPA correspondientes y realiza las consultas necesarias para obtener los vehículos de la base de datos, creando un repositorio para cada entidad.
Hazlo con una base de datos H2 orientada a fichero.
La interface VehiculoRepository debe tener, al menos los siguientes métodos:
Uno que devuelve los vehículos de una marca.
findAll: devuelve todos los vehículos, pero paginados.
existsByIdVehiculoAndVehiculoType: que devuelve si existe un vehículo con un id y un tipo de vehículo concreto. Para hacerlo debe tener un parámetro de tipo VehiculoType que es de tipo Class y un idVehiculo. Debe hacerse con una consulta personalizada que emplee la anotación @Query, con la consulta entre comillas y los parámetros entre dos puntos.
Un método que devuelva un VehiculoDTO con el idVehiculo, marca, modelo, año, precio y propietario de un vehículo concreto a partir del idVehiculo.
Un método que devuelva todos los vehiculos en formato VehiculoDTO, a ser posible paginados y con un orden concreto.
En este ejemplo crearemos una aplicación que accede a datos JPA relacionales a través de una interfaz frontal RESTful basada en Web.
2. Objetivo
Construir una aplicación Spring que te permite crear y recuperar objetos Persona almacenados en una base de datos utilizando Spring Data REST. Spring Data REST combina automáticamente las características de Spring HATEOAS y Spring Data JPA.
Spring Data REST también admite Spring Data Neo4j, Spring Data Gemfire y Spring Data MongoDB como almacenes de datos en el backend.
3. Requisitos
Un IDE.
Java 17 o posterior.
Gradle 7.5+ o Maven 3.5+.
Como sabes, también se puede importar el código directamente en uno de estos IDE:
Spring Tool Suite (STS).
IntelliJ IDEA Ultimate.
VSCode.
4. Creación del proyecto
Para inicializar el proyecto manualmente:
Ve a https://start.spring.io. Este servicio incluye todas las dependencias que necesitas para una aplicación y realiza la mayor parte de la configuración por ti.
Elige Maven y Java.
Haz clic en Dependencies y selecciona Rest Repositories, Spring Data JPA, y H2 Database.
Haz clic en Generate.
Descarga el archivo ZIP resultante, que es un archivo de una aplicación web configurada con lo seleccionado.
Si el IDE tiene la integración de Spring Initializr, puedes completar este proceso desde el IDE (Visual Studio Code, Eclipse).
También puedes bifurcar el proyecto desde Github y abrirlo en tu IDE u otro editor.
Crear un Objeto de Dominio
Crea una entidad para representar a una Persona, como muestra el siguiente listado (en src/main/java/local/sanclemente/accesorest/Persona.java):
El objeto Persona tiene un nombre y un apellido. (También hay un objeto ID configurado para generarse automáticamente).
5. Creación de un Repositorio de Persona
Luego, necesitas crear un repositorio sencillo, como muestra el siguiente listado (en src/main/java/local/sanclemente/accesorest/PersonaRepository.java):
package local.sanclemente.accesorest;
import java.util.List;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(collectionResourceRel ="gente", path ="gente")
// En realidad podrías emplear directamente JpaRepository que extiende de ListPagingAndSortingRepository y ListCrudRepositorypublicinterfacePersonaRepositoryextends PagingAndSortingRepository<Persona, Long>, CrudRepository<Persona,Long> {
List<Persona>findByApellido(@Param("nombre") String nombre);
}
Este repositorio es una interfaz que te permite realizar varias operaciones con objetos Persona.
Obtiene estas operaciones al heredar la interfaz PagingAndSortingRepository que está definida en Spring Data Commons.
En tiempo de ejecución, Spring Data REST crea automáticamente una implementación de esta interfaz.
Luego usa la anotación @RepositoryRestResource para dirigir a Spring MVC a crear puntos finales RESTful en /gente.
@RepositoryRestResourceno es necesario para que un repositorio se exporte. Se utiliza solo para cambiar los detalles de exportación, como el uso de /gente en lugar del valor predeterminado /Personas.
Aquí también has definido una consulta personalizada para recuperar una lista de objetos Persona basada en el apellido. Puedes ver cómo invocarla más adelante.
@SpringBootApplication es una anotación de conveniencia que agrega lo siguiente:
@Configuration: Etiqueta la clase como una fuente de definiciones de beans para el contexto de la aplicación.
@EnableAutoConfiguration: Indica a Spring Boot que comience a agregar beans en función de la configuración de la clase de ruta de clase, otros beans y diversas configuraciones de propiedad. Por ejemplo, si spring-webmvc está en la ruta de clase, esta anotación indica que la aplicación es una aplicación web y activa comportamientos clave, como configurar un DispatcherServlet.
@ComponentScan: Indica a Spring que busque otros componentes, configuraciones y servicios en el paquete com/example, permitiéndole encontrar los controladores.
El método main() utiliza el método SpringApplication.run() de Spring Boot para iniciar una aplicación. ¿Notaste que no hubo una sola línea de XML? Tampoco hay un archivo web.xml. Esta aplicación web es 100% Java puro y no tuviste que lidiar con la configuración de la infraestructura.
Spring Boot inicia automáticamente Spring Data JPA para crear una implementación concreta de PersonaRepository y la configura para hablar con una base de datos en memoria utilizando JPA.
Spring Data REST se basa en Spring MVC. Crea una colección de controladores Spring MVC, convertidores JSON y otros beans para proporcionar un frente RESTful. Estos componentes se vinculan al backend de Spring Data JPA. Cuando usas Spring Boot, todo esto está autoconfigurado. Si deseas investigar cómo funciona, puedes mirar la clase RepositoryRestMvcConfiguration en Spring Data REST.
6. Construir un JAR ejecutable
Puedes ejecutar la aplicación desde la línea de comandos con Gradle o Maven.
También puedes construir un solo archivo JAR ejecutable que contenga todas las dependencias necesarias, clases y recursos y ejecutar eso. Construir un JAR ejecutable facilita su envío, versión e implementación del servicio como una aplicación a lo largo del ciclo de desarrollo, en diferentes entornos, y así sucesivamente.
Si usas Gradle, puedes ejecutar la aplicación con ./gradlew bootRun. Alternativamente, puedes construir el archivo JAR con ./gradlew build y luego ejecutar el archivo JAR, así:
java -jar build/libs/{project_id}-0.1.0.jar
Si usas Maven, puedes ejecutar la aplicación con ./mvnw spring-boot:run. Alternativamente, puedes construir el archivo JAR con ./mvnw clean package y luego ejecutar el archivo JAR, así:
java -jar target/{project_id}-0.1.0.jar
Los pasos descritos aquí crean un JAR ejecutable. También puedes construir un archivo WAR clásico.
Se muestra la salida de registro. El servicio debería estar en funcionamiento en unos pocos segundos.
7. Probar la Aplicación
Ahora que la aplicación está en funcionamiento, puedes probarla. Puedes usar cualquier cliente REST que desees (o un navegador). Los siguientes ejemplos usan la herramienta *nix, curl (puedes descargar la versión portable de Windows desde aquí).
Primero, quieres ver el servicio de nivel superior. El siguiente ejemplo muestra cómo hacerlo:
El ejemplo anterior proporciona una primera visión de lo que ofrece este servidor. Hay un enlace gente ubicado en http://localhost:8080/gente. Tiene algunas opciones, como ?page, ?size, y ?sort.
Spring Data REST utiliza el formato HAL para la salida JSON. Es flexible y ofrece una forma conveniente de suministrar enlaces junto a los datos que se sirven.
El siguiente ejemplo muestra cómo ver los registros de Persona (ninguno por el momento):
-i: Asegura que puedes ver el mensaje de respuesta incluyendo las cabeceras. Se muestra la URI del recurso recién creado.
-H "Content-Type:application/json": Establece el tipo de contenido para que la aplicación sepa que la carga útil contiene un objeto JSON.
-d '{"nombre": "Frodo", "apellido": "Baggins"}': Es los datos que se envían.
Si estás en Windows, el comando anterior funcionará en WSL. Si no puedes instalar WSL, es posible que necesites reemplazar las comillas simples con comillas dobles y escapar las comillas dobles existentes, es decir, -d "{\"nombre\": \"Frodo\", \"apellido\": \"Baggins\"}".
Observa cómo la respuesta a la operación POST incluye una cabecera Location. Esto contiene la URI del recurso recién creado.
Spring Data REST también tiene dos métodos (RepositoryRestConfiguration.setReturnBodyOnCreate(…) y setReturnBodyOnUpdate(…)) que puedes usar para configurar el marco para que devuelva inmediatamente la representación del recurso recién creado. RepositoryRestConfiguration.setReturnBodyForPutAndPost(…) es un método abreviado para habilitar respuestas de representación para operaciones de creación y actualización.
7.2. Consulta de un registro
Puedes consultar todas las Persona, como muestra el siguiente ejemplo:
El objeto gente contiene una lista que incluye a Frodo. Observa cómo incluye un enlace self. Spring Data REST también utiliza Evo Inflector para pluralizar el nombre de la entidad para agrupamientos.
Puedes consultar directamente el registro individual, así:
Esto podría parecer puramente basado en la web. Sin embargo, detrás de escena, hay una base de datos relacional H2. En producción, recomendaría usar PostgreSQL.
Con un sistema más complejo, donde los objetos de dominio están relacionados entre sí, Spring Data REST renderiza enlaces adicionales para ayudar a navegar a registros conectados.
Puedes realizar todas las consultas personalizadas, como se muestra en el siguiente ejemplo:
Puedes ver la URL de la consulta, incluido el parámetro de consulta HTTP, nombre. Ten en cuenta que esto coincide con la anotación @Param("nombre") incrustada en la interfaz.
El siguiente ejemplo muestra cómo usar la consulta findByApellido:
Debido a que la has definido para devolver List<Persona> en el código, devuelve todos los resultados. Si la hubieras definido para devolver solo Persona, elegiría uno de los objetos Persona para devolver. Dado que esto puede ser impredecible, probablemente es mejor no hacerlo para consultas que puedan devolver múltiples entradas.
7.3. Actualización de registros
También se pueden emitir llamadas REST de PUT, PATCH y DELETE para reemplazar, actualizar o eliminar registros existentes (respectivamente). El siguiente ejemplo usa una llamada PUT:
Un aspecto conveniente de esta interfaz basada en hipertexto es que puedes descubrir todos los puntos finales RESTful usando curl (o cualquier cliente REST que prefieras).
No es necesario intercambiar un contrato o documento de interfaz formal con tus clientes.
Los repositorios son la abstracción que Spring Data utiliza para interactuar con las bases de datos, reduciendo la cantidad de bloques de código en tu aplicación.
Las clases DAO que interactúan con la base de datos y mapean los resultados, todo en la misma capa, pero son demasiado grandes y complejos de seguir la lógica.
Los repositorios no incluyen código de lógica de negocio, solo la declaración de los métodos para interactuar con la base de datos.
Spring Data ofrece una lista de repositorios (todos los cuales son interfaces que puedes extender), indicando la entidad y su tipo de ID. En tiempo de ejecución, el framework crea una clase proxy con toda la lógica necesaria para acceder a la base de datos.
En el ejemplo, BookRepository extiende de CrudRepository<T, ID>, que proporciona un conjunto de métodos para ejecutar operaciones CRUD en la base de datos:
Algunos de los métodos de esta interfaz padre son:
package org.springframework.data.repository;
import java.util.Optional;
@NoRepositoryBeanpublicinterfaceCrudRepository<T, ID>extends Repository<T, ID> {
<S extends T> S save(S entity); // Guarda o actualiza la entidad Optional<T>findById(ID primaryKey);
Iterable<T>findAll();
longcount();
voiddelete(T entity);
booleanexistsById(ID primaryKey);
// ... otros métodos más.}
Otra interfaz es PagingAndSortingRepository<T, ID>, que extiende de CrudRepository, JpaRepository y MongoRepository.
Cada uno de los dos últimos repositorios se utiliza para un tipo específico de base de datos en lugar de CrudRepository y PagingAndSortingRepository, que son interfaces genéricas para todas las bases de datos.
En Spring Data 3.0.0, apareció un nuevo repositorio. ListCrudRepository<T, ID> incluye métodos adicionales para recuperar una lista de elementos en lugar de una interfaz Iterable.
Los métodos que proporcionan los repositorios más comunes son útiles en la mayoría de los casos. Pero ¿qué sucede si necesitas encontrar un elemento por otra propiedad, no solo por el ID? Spring Data proporciona un mecanismo para crear consultas personalizadas sin crear clases adicionales, simplemente definiendo un método en la interfaz del repositorio con un nombre específico.
1.1. Consultas personalizadas automáticas
Spring Data analiza cada repositorio, buscando todos los métodos definidos para generar una consulta particular para cada uno de ellos. Si necesitas una consulta específica, puedes definir un nuevo método en la interfaz utilizando palabras clave que Spring Data utiliza para crear la consulta.
En este caso, necesitas crear un método que use findBy o existBy, seguido del nombre del campo que deseas buscar en la tabla. Spring lanza una excepción si el atributo no existe en la tabla:
Otras palabras clave permiten crear un conjunto de consultas combinando atributos, limitando la cantidad de resultados, u ordenando de una manera particular (ver Tabla).
La estructura de la consulta se divide en dos partes:
la primera define el sujeto de la consulta: consulta define qué tipo de operación la consulta necesita ejecutar.
la segunda es el predicado: es la parte de los atributos de la cláusula que filtra, ordena o es distinta.
El siguiente ejemplo utiliza una interfaz List en lugar de una interfaz Set para mostrarte que puedes tener elementos duplicados, pero también pudede cambiarse a una interfaz Set, la consulta sigue funcionando sin problemas:
Podemos ver qué genera Spring Data como consulta para acceder a la base de datos. Todas las aplicaciones cambian el valor de la propiedad show-sql de false a true en application.yml. Si ejecutas la aplicación y haces una solicitud, pueden verse las consultas SQL generadas en la consola.
select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.decimal_places as decimal_3_0_, book0_.titulo as descript4_0_, book0_.enabled as enabled5_0_ from book book0_ where book0_.isbn=?
List findByIsbnAndTitulo(String isbn, String titulo);
select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.decimal_places as decimal_3_0_, book0_.titulo as descript4_0_, book0_.enabled as enabled5_0_ from book book0_ where book0_.isbn=? and book0_.titulo=?
List findByTituloOrderByIsbnAsc(String titulo);
select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.decimal_places as decimal_3_0_, book0_.titulo as descript4_0_, book0_.enabled as enabled5_0_ from book book0_ where book0_.titulo=? order by book0_.isbn asc
List findByTituloOrderByIsbnDesc(String titulo);
select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.decimal_places as decimal_3_0_, book0_.titulo as descript4_0_, book0_.enabled as enabled5_0_ from book book0_ where book0_.titulo=? order by book0_.isbn desc
Ejemplos de palabras clave sujeto de consultas
Algunas de las palabras clave más comunes son (podrían no ser admitidas por bases de datos no relacionales):
Palabra clave
Descripción
findBy…
Estas palabras clave están generalmente asociadas con una consulta SELECT y devuelven un elemento o conjunto de elementos que pueden ser un subtipo de Collection o Streamable.
getBy…
queryBy…
countBy…
Devuelve el número de elementos que coinciden con la consulta.
existBy…
Devuelve un tipo booleano con verdadero si hay algo que coincide con la consulta.
deleteBy…
Elimina un conjunto de elementos que coinciden con la consulta pero no devuelve nada.
Ejemplos de palabras clave predicado de consultas
Palabra Clave
Expresiones de la Palabra Clave
LIKE
Like
IS_NULL
Null o IsNull
LESS_THAN
LessThan
GREATER_THAN
GreaterThan
AND
And
OR
Or
AFTER
After o IsAfter
BEFORE
Before o IsBefore
1.2 Consultas personalizadas manuales
La segunda forma de crear consultas para acceder a una base de datos es el método clásico: escribir la consulta que necesitas ejecutar en un formato similar a SQL y definir un método en la interfaz. El repositorio anterior para incluir una consulta manual que encuentra un elemento usando el código:
import com.javhoz.biblioteca.Book;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
publicinterfaceBookRepositoryextends CrudRepository<Book, Long> {
// Consultas generales List<Book>findByIsbn(String code);
List<Book>findByIsbnAndTitulo(String code, String titulo);
// Consultas de orden List<Book>findByTituloOrderByIsbnAsc(String titulo);
List<Book>findByTituloOrderByIsbnDesc(String titulo);
// Consulta manual@Query("SELECT c FROM Book c WHERE c.isbn = :isbn")
Book retrieveByIsbn(@Param("isbn") String isbn);
}
Hay muchas formas de declarar una consulta:
Puedes declararla como constante en la parte superior de la interfaz para tener todas las declaraciones de métodos y entender cada una de ellas.
Externaliza todas las consultas en un archivo de propiedades e impórtalas dinámicamente en cada repositorio. Una de las desventajas de este enfoque es que necesitas tener una buena organización para saber qué archivo contiene las consultas de cada repositorio.
Puedes tener una clase que contenga todas las consultas de un repositorio específico. Este enfoque es útil cuando tienes muchas consultas que son demasiado largas. Limpia tu repositorio para tener solo los métodos en un lugar y todas las consultas en otro lugar. Además, define patrones de nombres para identificar la idea detrás de cada consulta.
Hay muchas ventajas y desventajas con cada uno de estos enfoques.
¿Por qué necesitas crear una consulta manual si hay una forma de hacerlo automáticamente? Una respuesta es que necesitas mejorar el rendimiento de la consulta que Spring Data genera, o no necesitas todos los atributos de la tabla. Cubres un escenario específico. Esta situación tiene el nombre de Proyecciones.
Otra situación es que la consulta es tan compleja que no existe una palabra clave para expresarla. No hay una regla que explique todos los escenarios potenciales cuando necesitas usar un mecanismo en lugar de otro. Pero si sabes que la aplicación tiene un problema con el rendimiento de la consulta, la mejor alternativa podría ser intentar escribir la consulta manualmente y ver qué sucede.
05. Paginación y Ordenación usando Spring Data JPA.
La paginación y la ordenación son características esenciales para manejar grandes conjuntos de datos en aplicaciones. Spring Data JPA ofrece una forma sencilla y poderosa para implementar estas características.
1. Paginación
La paginación te permite dividir grandes resultados en partes más pequeñas y manejables, llamadas páginas. Spring Data JPA proporciona una clase PageRequest para crear solicitudes de paginación:
Para utilizar la paginación, necesitas extender tu repositorio de PagingAndSortingRepository o JpaRepository. Estas interfaces heredan de CrudRepository y añaden métodos de paginación y ordenación.
Aquí hay un ejemplo de cómo definir un repositorio con paginación:
Spring Data JPA simplifica significativamente la implementación de paginación y ordenación. Utilizando las interfaces y clases proporcionadas, puedes manejar grandes conjuntos de datos de manera eficiente y mejorar la experiencia del usuario en tu aplicación.
Paginación y Ordenación usando Spring Data JPA
La paginación es útil cuando tenemos un gran conjunto de datos y queremos presentarlo al usuario en partes más pequeñas. Además, a menudo necesitamos ordenar esos datos por algún criterio mientras paginamos.
En este apartado veremos un ejemplo completo de cómo paginar y ordenar fácilmente usando Spring Data JPA.
1. Configuración Inicial
Primero, supongamos que tenemos una entidad Producto como nuestra clase de dominio:
Al extender PagingAndSortingRepository, obtenemos los métodos findAll(Pageable pageable) y findAll(Sort sort) para paginación y ordenación.
También podríamos haber elegido extender JpaRepository, ya que también extiende PagingAndSortingRepository.
Una vez que extendemos PagingAndSortingRepository, podemos agregar nuestros propios métodos que toman Pageable y Sort como parámetros, como hicimos aquí con findAllByPrecio.
Veamos cómo paginar productos usando nuestro nuevo método.
3. Paginación
Una vez que tenemos nuestro repositorio extendido de PagingAndSortingRepository, solo necesitamos:
Crear u obtener un objeto PageRequest, que es una implementación de la interfaz Pageable.
Pasar el objeto PageRequest como argumento al método del repositorio que queremos usar.
Podemos crear un objeto PageRequest pasando el número de página solicitado y el tamaño de la página.
El método findAll(Pageable pageable) devuelve por defecto un objeto Page<T>.
Sin embargo, podemos optar por devolver un Page<T>, un Slice<T> o un List<T> desde cualquiera de nuestros métodos personalizados que devuelven datos paginados.
Una instancia de Page<T>, además de tener la lista de productos, también conoce el número total de páginas disponibles. Para lograr esto, desencadena una consulta de conteo adicional. Para evitar dicho costo adicional, podemos devolver un Slice<T> o un List<T> en su lugar.
Un Slice solo sabe si la siguiente porción está disponible o no.
4. Paginación y Ordenación
De manera similar, para solo tener nuestros resultados de consulta ordenados, podemos simplemente pasar una instancia de Sort al método:
Basándonos en nuestros requisitos de ordenación, podemos especificar los campos de ordenación y la dirección de ordenación al crear nuestra instancia de PageRequest.
Esto cubre cómo puedes utilizar la paginación y la ordenación en Spring Data JPA para manejar grandes conjuntos de datos de manera eficiente y mejorar la experiencia del usuario en tu aplicación.
06. Paginación y Ordenación usando Spring Data REST.
1. Paginación y Ordenación de Spring Data Repository en Spring Data REST
Esta sección documenta el uso de las abstracciones de paginación y ordenación de Spring Data Repository en Spring Data REST.
1.1 Paginación
En lugar de devolver todo desde un conjunto de resultados grande, Spring Data REST reconoce algunos parámetros de URL que influyen en el tamaño de la página y el número de página inicial.
Si heredas de PagingAndSortingRepository<T, ID> y accedes a la lista de todas las entidades, obtienes enlaces a las primeras 20 entidades. Para establecer el tamaño de la página en cualquier otro número, añade un parámetro size:
http://localhost:8080/gente/?size=5
En el ejemplo anterior se establece el tamaño de la página en 5.
Para usar la paginación en métodos de consulta personalizados, los que has declarado, se necesita cambiar la firma del método para aceptar un parámetro adicional Pageable y devolver una Page o Slice en lugar de una List.
Por ejemplo, el siguiente método de consulta se exporta a /gente/search/nomeStartsWith y admite paginación:
El exportador de Spring Data REST reconoce la Page/Slice devuelta y te da los resultados en el cuerpo de la respuesta, tal como lo haría con una respuesta no paginada, pero se añaden enlaces adicionales al recurso para representar las páginas anteriores y siguientes de datos.
1.3. Enlaces Anterior y Siguiente
Cada respuesta paginada devuelve enlaces a las páginas anteriores y siguientes de resultados basados en la página actual utilizando las relaciones de enlace definidas por IANA prev y next. Sin embargo, si estás en la primera página de resultados, no se renderiza el enlace prev. Para la última página de resultados, no se renderiza el enlace next.
Considera el siguiente ejemplo, donde establecemos el tamaño de la página en 5:
El enlace self sirve toda la colección con algunas opciones.
El enlace next apunta a la siguiente página, asumiendo el mismo tamaño de página.
En la parte inferior, hay datos adicionales sobre la configuración de la página, incluido el tamaño de la página, el total de elementos, el total de páginas y el número de página que estás viendo actualmente.
Al usar herramientas como curl en la línea de comandos, si tienes un ampersand (&) en tu declaración, necesitas envolver toda la URI entre comillas.
Ten en cuenta que las URIs self y next son, de hecho, plantillas de URI. Aceptan no solo size, sino también page y sort como banderas opcionales.
Como se mencionó anteriormente, la parte inferior del documento HAL incluye una colección de detalles sobre la página. Esta información adicional facilita la configuración de herramientas de interfaz de usuario como deslizadores o indicadores para reflejar la posición general del usuario cuando visualiza los datos. Por ejemplo, el documento en el ejemplo anterior muestra que estamos viendo la primera página (con números de página que comienzan en 0).
El siguiente ejemplo muestra qué sucede cuando seguimos el enlace next:
Esto se ve muy similar, excepto por las siguientes diferencias:
El enlace next ahora apunta a otra página, indicando su perspectiva relativa al enlace self.
Aparece un enlace prev, dándonos un camino a la página anterior.
El número actual es ahora 1 (indicando la segunda página).
Esta característica te permite mapear botones opcionales en la pantalla a estos controles de hipermedia, permitiéndote implementar características de navegación para la experiencia de usuario sin tener que codificar las URIs. De hecho, el usuario puede elegir de una lista de tamaños de página, cambiando dinámicamente el contenido servido, sin tener que reescribir los controles next y prev en la parte superior o inferior.
1.2. Ordenación
Spring Data REST reconoce los parámetros de ordenación que usan el soporte de ordenación del repositorio.
Para que tus resultados se ordenen en una propiedad particular, agrega un parámetro de URL sort con el nombre de la propiedad en la que deseas ordenar los resultados. Puedes controlar la dirección de la ordenación añadiendo una coma (,) al nombre de la propiedad más asc o desc. El siguiente ejemplo usaría el método de consulta findByNomeStartsWith definido en el PersonaRepository para todas las entidades Person con nombres que comienzan con la letra “K” y añadiría datos de ordenación que ordenan los resultados en la propiedad name en orden descendente:
Para ordenar los resultados por más de una propiedad, sigue añadiendo tantos parámetros sort=PROPERTY como necesites. Se añaden al Pageable en el orden en que aparecen en la cadena de consulta. Los resultados pueden ordenarse por propiedades de nivel superior y anidadas. Usa la notación de ruta de propiedades para expresar una propiedad de ordenación anidada. La ordenación por asociaciones vinculables (es decir, enlaces a recursos de nivel superior) no es compatible.
2. Ejemplo de paginación REST en Spring Data
En Spring Data, si necesitamos devolver algunos resultados del conjunto de datos completo, podemos usar cualquier método de repositorio de Pageable, ya que siempre devolverá una Page. Los resultados se devolverán según el número de página, el tamaño de la página y la dirección de ordenación.
Spring Data REST reconoce automáticamente parámetros de URL como page, size, sort, etc.
Para usar los métodos de paginación de cualquier repositorio, necesitamos heredar PagingAndSortingRepository:
Por defecto, el tamaño de la página es 20, pero podemos cambiarlo llamando a algo como http://localhost:8080/asuntos?size=10.
Si queremos implementar la paginación en nuestra propia API de repositorio personalizada, necesitamos pasar un parámetro adicional Pageable y asegurarnos de que la API devuelva una Page:
@RestResource(path ="nombreContains")
public Page<Asunto>findByNombreContaining(@Param("nombre") String nombre, Pageable p);
Cada vez que agregamos una API personalizada, se agrega un endpoint /search a los enlaces generados. Entonces, si llamamos a http://localhost:8080/asuntos/search, veremos un endpoint capaz de paginación:
Todas las APIs que implementan PagingAndSortingRepository devolverán una Page. Si necesitamos devolver la lista de resultados de la Page, la API getContent() de Page proporciona la lista de registros obtenidos como resultado de la API de Spring Data REST.
10. Convertir una Lista en una Página
Supongamos que tenemos un objeto Pageable como entrada, pero la información que necesitamos recuperar está contenida en una lista en lugar de un PagingAndSortingRepository. En estos casos, es posible que necesitemos convertir una List en una Page.
Por ejemplo, imagina que tenemos una lista de resultados de un servicio SOAP:
List<Foo> list = getListOfFooFromSoapService();
Necesitamos acceder a la lista en las posiciones específicas especificadas por el objeto Pageable que se nos envía. Así que definamos el índice de inicio:
int start = (int) pageable.getOffset();
Y el índice final:
int end = (int) ((start + pageable.getPageSize()) > fooList.size() ? fooList.size()
: (start + pageable.getPageSize()));
Teniendo estos dos en su lugar, podemos crear una Page para obtener la lista de elementos entre ellos:
Ahora podemos devolver page como un resultado válido.
Y ten en cuenta que si también queremos soportar la ordenación, necesitamos ordenar la lista antes de crear la sublista.
3. Paginación REST en Spring avanzada
3.1. Descripción general
Este apartado veremos cómo implementar la paginación en una API REST utilizando Spring MVC y Spring Data.
3.2. Página como Recurso vs Página como Representación
La primera pregunta al diseñar la paginación en el contexto de una arquitectura RESTful es si considerar la página como un recurso real o simplemente como una representación de los recursos.
Tratar la página en sí misma como un recurso introduce una serie de problemas, como la imposibilidad de identificar recursos de manera única entre llamadas. Esto, junto con el hecho de que, en la capa de persistencia, la página no es una entidad adecuada sino un contenedor que se construye cuando es necesario, hace que la elección sea clara; la página es parte de la representación.
La siguiente pregunta en el diseño de la paginación en el contexto de REST es dónde incluir la información de paginación:
En la ruta URI: /foo/page/1
En la consulta URI: /foo?page=1
Teniendo en cuenta que una página no es un recurso, codificar la información de la página en la URI no es una opción.
Veremos la forma estándar de resolver este problema codificando la información de paginación en una consulta URI.
3.3. El Controlador
MVC vs REST
Cuando no empleamos un marco de trabajo como Spring Data REST, la paginación se convierte en una tarea manual. En este caso, necesitamos implementar la paginación en la capa de servicio y devolver una lista paginada al controlador.
Spring Data Rest proporciona una forma más sencilla de implementar la paginación, ya que la paginación se maneja automáticamente.
Ahora, para la implementación. El controlador de Spring MVC para la paginación es relativamente sencillo:
En este ejemplo, estamos inyectando los dos parámetros de consulta, size y page, en el método del controlador a través de @RequestParam.
Alternativamente, podríamos haber usado un objeto Pageable, que mapea automáticamente los parámetros de página, tamaño y orden. Además, la entidad PagingAndSortingRepository proporciona métodos inmediatos para usar que admiten el uso de Pageable como parámetro.
También estamos inyectando la respuesta HTTP y el UriComponentsBuilder para ayudar con la descubribilidad, que estamos desacoplando mediante un evento personalizado. Si eso no es un objetivo de la API, simplemente podemos eliminar el evento personalizado.
Finalmente, nota que el enfoque de este apartado es solo la capa REST y web; para profundizar en la parte de acceso a datos de la paginación, podemos consultar los apartados anteriores con Spring Data.
3.4. Cómo descubrir la Paginación REST
Dentro del alcance de la paginación, satisfacer la restricción HATEOAS de REST significa permitir que el cliente de la API descubra las páginas siguiente y anterior basándose en la página actual en la navegación. Para este propósito, utilizaremos el encabezado HTTP Link, junto con los tipos de relación de enlace “next”, “prev”, “first” y “last”.
En REST, la “descubribilidad” es una preocupación transversal, aplicable no solo a operaciones específicas, sino a tipos de operaciones. Por ejemplo, cada vez que se crea un recurso, el URI de ese recurso debería ser descubrible por el cliente. Dado que este requisito es relevante para la creación de CUALQUIER recurso, lo manejaremos por separado.
Desacoplaremos estas preocupaciones usando eventos, como discutimos en el artículo anterior centrado en la descubribilidad de un servicio REST. En el caso de la paginación, el evento, PaginatedResultsRetrievedEvent, se dispara en la capa del controlador. Luego implementaremos la descubribilidad con un oyente personalizado para este evento.
En resumen, el oyente comprobará si la navegación permite páginas siguiente, anterior, primera y última. Si lo hace, agregará las URIs relevantes a la respuesta como un encabezado HTTP ‘Link’.
Ahora vamos paso a paso. El UriComponentsBuilder pasado desde el controlador contiene solo la URL base (el host, el puerto y la ruta de contexto). Por lo tanto, tendremos que agregar las secciones restantes:
voidaddLinkHeaderOnPagedResourceRetrieval( UriComponentsBuilder uriBuilder, HttpServletResponse response, Class clazz, int page, int totalPages, int size ){
String resourceName = clazz.getSimpleName().toString().toLowerCase();
uriBuilder.path( "/admin/"+ resourceName );
// ...}
A continuación, utilizaremos un StringJoiner para concatenar cada enlace. Usaremos el uriBuilder para generar las URIs. Veamos cómo procedemos con el enlace a la siguiente página:
Nota que, por brevedad, solo se incluye una muestra parcial del código y el código completo está aquí.
3.5. Pruebas de la Paginación
Tanto la lógica principal de la paginación como la descubribilidad están cubiertas por pruebas de integración pequeñas y enfocadas. Como en el artículo anterior, utilizaremos la biblioteca REST-assured para consumir el servicio REST y verificar los resultados.
Estos son algunos ejemplos de pruebas de integración de paginación; para una suite de pruebas completa, consulta el proyecto en GitHub (enlace al final del artículo):
Nota que el código completo de bajo nivel para extractURIByRel, responsable de extraer las URIs por relación rel, está aquí.
3.7. Obtener Todos los Recursos
Sobre el mismo tema de paginación y descubribilidad, se debe tomar la decisión de
si se permite al cliente recuperar todos los recursos del sistema de una vez, o si el cliente debe solicitarlos paginados.
Si se decide que el cliente no puede recuperar todos los recursos con una sola solicitud, y se requiere paginación, entonces varias opciones están disponibles para la respuesta de una solicitud. Una opción es devolver un 404 (Not Found) y usar el encabezado Link para hacer que la primera página sea descubrible:
Otra opción es devolver una redirección, 303 (See Other), a la primera página. Una ruta más conservadora sería simplemente devolver al cliente un 405 (Method Not Allowed) para la solicitud GET.
3.8. Paginación REST con Encabezados HTTP Range
Una forma relativamente diferente de implementar la paginación es trabajar con los encabezados HTTP Range: Range, Content-Range, If-Range, Accept-Ranges, y códigos de estado HTTP, 206 (Partial Content), 413 (Request Entity Too Large) y 416 (Requested Range Not Satisfiable).
Una visión de este enfoque es que las extensiones del rango HTTP no están destinadas para la paginación y deben ser manejadas por el servidor, no por la aplicación. Implementar la paginación basada en las extensiones del encabezado HTTP Range es técnicamente posible, aunque no es tan común como la implementación discutida en este artículo.
3.9. Conclusión
Este apartado ilustró cómo implementar la paginación en una API REST utilizando Spring y discutió cómo configurar y probar la descubribilidad.
2. Creación de una interfaz de usuario (Web) CRUD con Vaadin
En esta práctica construiremos una aplicación que utiliza una interfaz de usuario (UI) basada en Vaadin en Frontend y Spring Data JPA en el Backend.
El objetivo es construir una interfaz de usuario Vaadin para un repositorio JPA, realizando una aplicación Web con funcionalidad completa de CRUD (Create, Read, Update, Delete) y un ejemplo de filtrado que utiliza un método personalizado del repositorio.
2.1. Requisitos
Entorno de Desarrollo Integrado (IDE) como IntelliJ IDEA, Eclipse o VSCode (NetBeans también es válido ;-) ).
Java 17 o superior.
Maven 3.5+.
Como hemos visto, también es posible emplear Spring Initializr para generar el proyecto con el IDE:
Spring Tool Suite (STS).
IntelliJ IDEA Ultimate.
VSCode.
2.2. Configuración del proyecto: Spring Initializr
Accede a https://start.spring.io. Permite añadir todas las dependencias necesarias para una aplicación y realiza la mayoría de la configuración.
Elige Maven y Java.
Haz clic en “Dependencies” y selecciona Vaadin, Spring Data JPA y H2 Database.
Haz clic en “Generate”.
Descarga el archivo ZIP resultante, que es un archivo comprimido de una aplicación web configurada con tus para trabajar con Vaadin, JPA y H" como sistema gestor de base de datos.
Si el IDE (Visual Studio Code, Eclipse o Intellj Ultimate) tiene integración con Spring Initializr, puedes hacer este proceso desde el IDE.
2.3. Servicios de Backend (entidades y repositorios)
En este punto necesitamos crear las clases para los objetos de entidad y repositorio.
La siguiente lista (del archivo src/main/java/local/sanclemente/ad/crudvaadin/Usuario.java) define la entidad del Usuario:
La siguiente lista (del archivo src/main/java/local/sanclemente/ad/crudvaadin/CrudvaadinApplication.java) muestra la clase de la aplicación, que crea algunos datos para ti:
package local.sanclemente.ad.crudvaadin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplicationpublicclassCrudvaadinApplication {
privatestaticfinal Logger log = LoggerFactory.getLogger(CrudvaadinApplication.class);
publicstaticvoidmain(String[] args) {
SpringApplication.run(CrudvaadinApplication.class);
}
/**
* Este método se ejecuta al inicio de la aplicación y carga algunos datos de ejemplo.
* @param repository
* @return CommandLineRunner
*/@Bean// Esta anotación le dice a Spring que ejecute este método al iniciopublic CommandLineRunner loadData(RepositorioUsuario repository) {
return (args) -> {
// creamos algunos usuarios con nombres de poetas y apellidos de personajes de sus obras repository.save(new Usuario("Federico", "Lorca"));
repository.save(new Usuario("Antonio", "Machado"));
repository.save(new Usuario("Miguel", "Hernández"));
repository.save(new Usuario("Gustavo", "Adolfo Bécquer"));
repository.save(new Usuario("Luis", "Cernuda"));
repository.save(new Usuario("Rafael", "Alberti"));
// Poetas norteamericanas del género de poería confesional: repository.save(new Usuario("Sylvia", "Plath"));
repository.save(new Usuario("Anne", "Sexton"));
repository.save(new Usuario("Sharon", "Olds"));
repository.save(new Usuario("Louise", "Glück"));
repository.save(new Usuario("Lucie", "Brock-Broido"));
repository.save(new Usuario("Jorie", "Graham"));
// REcoge todos los usuarios y los muestra en el log log.info("Poetas/poetisas encontrados/as con findAll():");
log
.info("-------------------------------");
for (Usuario usuario : repository.findAll()) {
log.info(usuario.toString());
}
log.info("");
// obtención de usuario por ID Usuario usuario = repository.findById(1L).get();
log.info("Usuario con findOne(1L):");
log.info("--------------------------------");
log.info(usuario.toString());
log.info("");
// fetch customers by last name log.info("Usuario encontrado con findByApellidosStartsWithIgnoreCase('Plath'):");
log.info("--------------------------------------------");
for (Usuario plath : repository.findByApellidosStartsWithIgnoreCase("Plath")) {
log.info(plath.toString());
}
log.info("");
};
}
}
CommadLineRunner es una interfaz funcional que se ejecuta al inicio de la aplicación. En este caso, se utiliza para cargar algunos datos de ejemplo en la base de datos.
Spring boor llama automáticamente a todos los beans CommandLineRunner una vez que el contexto de la aplicación está cargado y ejecuta el método run de cada bean.
Suele emplearse para realizar tareas de inicialización, como cargar datos de ejemplo en la base de datos. Se puede:
Crear un @Component que implemente CommandLineRunner.
Crear un @Bean que devuelva un CommandLineRunner.
Crear un @SpringBootApplication que implemente CommandLineRunner.
Crear un @Configuration que implemente CommandLineRunner.
Las dependencias ya esá configuradas. Sin embargo, en este apartado se describe cómo agregar el soporte de Vaadin a un proyecto Spring nuevo.
La integración de Vaadin en Spring contiene una colección de dependencias de inicio de Spring Boot, así que solo necesitas agregar el siguiente fragmento de Maven (o una configuración de Gradle correspondiente), que ya ha sido añadido por Spring Initializr:
El ejemplo utiliza una versión más nueva de Vaadin que la versión predeterminada traída por el módulo de inicio (Spring Initializr). Para usar una versión más reciente, define el Bill of Materials (BOM) de Vaadin de la siguiente manera:
En el modo de desarrollo, la dependencia es suficiente, mas cuando estás construyendo para producción, necesitas habilitar tu aplicación para compilaciones de producción.
Por defecto, Gradle no admite los BOM, pero hay un práctico complemento para eso. Consulta el archivo de construcción build.gradle para ver un ejemplo de cómo lograr lo mismo.
2.4. La vista: clase principal de la vista de Vaadin
La clase principal de la vista (que hemos llamado MainView) es el punto de entrada para la lógica de la interfaz de usuario de Vaadin. En las aplicaciones Spring Boot, si la anotas con @Route, se recoge automáticamente y se muestra en la raíz de tu aplicación web.
Se puede personalizar la URL donde se muestra la vista al dar un parámetro a la anotación @Route. La siguiente lista (del proyecto inicial en src/main/java/local/sanclemente/ad/crudvaadin/MainView.java) muestra una vista simple de saludo:
package local.sanclemente.ad.crudvaadin;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
@RoutepublicclassMainViewextends VerticalLayout {
publicMainView() {
add(new Button("Saluda", e -> Notification.show("¡Hola, estudiante de Acceso a Datos!")));
}
}
2.5. Listado de usuarios en un Grid de Datos
Existen muchos componentes visuales Vaadin:
https://vaadin.com/docs/latest/components, uno de ellos es el Grid, que es un componente de tabla de datos que muestra una lista de objetos en una tabla.
Para un diseño listado, puedes usar el componente Grid. Puedes pasar la lista de entidades desde un RepositorioUsuario inyectado por constructor al Grid usando el método setItems. El cuerpo de la clase MainView sería así:
@RoutepublicclassMainViewextends VerticalLayout {
privatefinal RepositorioUsuario repo;
final Grid<Usuario> grid;
publicMainView(RepositorioUsuario repo) {
this.repo= repo;
this.grid=new Grid<>(Usuario.class);
add(grid);
listarUsuarios();
}
privatevoidlistarUsuarios() {
// El método `findAll` es implícito en el RepositorioUsuario e implementado por Spring Data JPA grid.setItems(repo.findAll()); // Carga todos los usuarios }
}
Nota
Si disponemos de tablas grandes o muchos usuarios concurrentes, es mejor no vincular todo el conjunto de datos a tus componentes de UI.
Aunque Vaadin Grid carga perezosamente los datos del servidor al navegador, el enfoque anterior mantiene toda la lista de datos en la memoria del servidor. Para ahorrar algo de memoria, e pueden mostrar solo los resultados más importantes mediante la paginación o el uso de carga perezosa, por ejemplo, utilizando el método:
Se puede usar un componente TextField para crear una entrada de filtro. Para hacerlo, primero modifica el método listarUsuarios() para admitir el filtrado. El siguiente ejemplo (en src/main/java/local/sanclemente/ad/crudvaadin/MainView.java) muestra cómo hacerlo:
Aquí es donde son útiles las **consultas declarativas de Spring Data**. **Escribir `findByApellidosStartsWithIgnoringCase` es una definición de una sola línea en la interfaz RepositorioUsuario**.
Puedes añadir un listener al componente TextField y pasar su valor a ese método de filtro. El ValueChangeListener se llama automáticamente a medida que un usuario escribe porque defines el ValueChangeMode.LAZY en el campo de texto del filtro. El siguiente ejemplo muestra cómo configurar dicho listener:
2.7. Definición de un Editor de Usuarios personalizado
Como las UI de Vaadin son código Java, se puede escribir código reutilizable desde el principio.
Para hacerlo, se puede definir un componente editor para tu entidad Usuario. Puedes hacer que sea un bean administrado por Spring para que puedas inyectar directamente el RepositorioUsuario en el editor y abordar las partes de Crear, Actualizar y Eliminar de tu funcionalidad CRUD. El siguiente ejemplo (del archivo src/main/java/local/sanclemente/ad/crudvaadin/EditorUsuario.java) muestra cómo hacerlo:
package local.sanclemente.ad.crudvaadin;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyNotifier;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Un ejemplo sencillo para introducir la construcción de formularios. Dado que una aplicación real probablemente sea mucho más complicada que este ejemplo, podrías reutilizar este formulario en varios sitios. Este
* componente de ejemplo se utiliza únicamente en la vista principal, MainView.
* <p>
* En una aplicación del mundo real, es probable que utilices una superclase común para todos tus
* formularios: menos código, mejor experiencia de usuario.
*/@SpringComponent@UIScopepublicclassEditorUsuarioextends VerticalLayout implements KeyNotifier {
privatefinal RepositorioUsuario repository;
/**
* Usuario que estamos editando
*/private Usuario usuario;
/* Para editar las propiedades de la entidad Usuario */ TextField nombre =new TextField("Nombre");
TextField apellidos =new TextField("Apellidos");
/* Botones de acción */ Button guardar =new Button("Guardar", VaadinIcon.CHECK.create());
Button cancel =new Button("Cancelar");
Button delete =new Button("Borrar", VaadinIcon.TRASH.create());
HorizontalLayout accionesLayout =new HorizontalLayout(guardar, cancel, delete);
// Binder para enlazar propiedades y campos Binder<Usuario> binder =new Binder<>(Usuario.class);
private ChangeHandler changeHandler;
@AutowiredpublicEditorUsuario(RepositorioUsuario repository) {
this.repository= repository;
add(nombre, apellidos, accionesLayout);
// enlace de los campos con las propiedades de la entidad Usuario binder.bindInstanceFields(this);
// Configuración y estilos de los botones setSpacing(true);
guardar.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
delete.addThemeVariants(ButtonVariant.LUMO_ERROR);
addKeyPressListener(Key.ENTER, e -> guardar());
// Escucha cambios realizados por el editor, actualiza la lista de usuarios guardar.addClickListener(e -> guardar());
delete.addClickListener(e -> delete());
cancel.addClickListener(e -> editUsuario(usuario));
setVisible(false);
}
voiddelete() {
repository.delete(usuario);
changeHandler.onChange();
}
voidguardar() {
repository.save(usuario);
changeHandler.onChange();
}
publicinterfaceChangeHandler {
voidonChange();
}
publicfinalvoideditUsuario(Usuario c) {
if (c ==null) {
setVisible(false);
return;
}
finalboolean existePersistido = c.getIdUsuario() !=null;
if (existePersistido) {
// Busca la entidad actualizada con el mismo ID// En una aplicación del mundo real, esto debería comprobar si realmente existe en la base de datos// Con carga perezosa para las relaciones con la entidad. usuario = repository.findById(c.getIdUsuario()).get();
}
else {
usuario = c;
}
cancel.setVisible(existePersistido);
// Enlaza las propiedades del usuario con los nombres similares de los nombres de los campos.// Podría usaser una anotación o "manual binding" o por medio de programación// moviendo los valores de los campos a la entidad y viceversa. binder.setBean(usuario);
setVisible(true);
// Enfoca el nombre inicialmente nombre.focus();
}
publicvoidsetChangeHandler(ChangeHandler h) {
// ChangeHandler es notificado cuando guardas o borras haciendo clic. changeHandler = h;
}
}
En una aplicación más grande, podrías usar este componente editor en varios lugares. También ten en cuenta que, en aplicaciones grandes, es posible que desees aplicar algunos patrones comunes (como MVP) para estructurar tu código de UI.
2.8. Conectar el Editor de manera bidireccional
En los pasos anteriores, ya has visto algunos conceptos básicos de la programación basada en componentes. Al usar un botón y agregar un listener de selección a la cuadrícula, puedes integrar completamente tu editor en la vista principal. La siguiente lista (del archivo src/main/java/local/sanclemente/ad/crudvaadin/MainView.java) muestra la versión final de la clase MainView:
package local.sanclemente.ad.crudvaadin;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Route;
import org.springframework.util.StringUtils;
@RoutepublicclassMainViewextends VerticalLayout {
privatefinal RepositorioUsuario repo;
privatefinal EditorUsuario editor;
final Grid<Usuario> grid;
final TextField filtro;
privatefinal Button addNewBtn;
publicMainView(RepositorioUsuario repo, EditorUsuario editor) {
this.repo= repo;
this.editor= editor;
this.grid=new Grid<>(Usuario.class);
this.filtro=new TextField();
this.addNewBtn=new Button("Nuevo usuario", VaadinIcon.PLUS.create());
// Disposición de los componentes: HorizontalLayout accionesLayout =new HorizontalLayout(filtro, addNewBtn);
add(accionesLayout, grid, editor);
grid.setHeight("300px");
grid.setColumns("idUsuario", "nombre", "apellidos");
grid.getColumnByKey("idUsuario").setWidth("50px").setFlexGrow(0);
filtro.setPlaceholder("Filtrar por apellidos");
// Enlace de la lógica con los componentes// Sustituye el listado cuando se aplica un filtro: filtro.setValueChangeMode(ValueChangeMode.LAZY);
filtro.addValueChangeListener(e -> listarUsuarios(e.getValue()));
// Conecta el Usuario seleccionado al editor o lo oculta si no está seleccionado grid.asSingleSelect().addValueChangeListener(e -> {
editor.editUsuario(e.getValue());
});
// Instancia y edita un nuevo Usuario cuando se pulsa el botón "Nuevo Usuario" addNewBtn.addClickListener(e -> editor.editUsuario(new Usuario("", "")));
// Escucha los cambios hechos por el editor, refresca los datos del modelo. editor.setChangeHandler(() -> {
editor.setVisible(false);
listarUsuarios(filtro.getValue());
});
// Inicializa el listado listarUsuarios(null);
}
// tag::listarUsuarios[]voidlistarUsuarios(String textoFiltro) {
if (StringUtils.hasText(textoFiltro)) {
grid.setItems(repo.findByApellidosStartsWithIgnoreCase(textoFiltro));
} else {
grid.setItems(repo.findAll());
}
}
// end::listarUsuarios[]}
2.9. Construir un JAR Ejecutable
Puedes ejecutar la aplicación desde la línea de comandos con Gradle o Maven. También puedes construir un solo archivo JAR ejecutable que contenga todas las dependencias, clases y recursos necesarios y ejecutarlo.
Construir un JAR ejecutable facilita el transporte, la copia de seguridad y el intercambio con otros.
Para construir un JAR ejecutable, ejecute el siguiente comando:
./gradlew clean build
o
./mvnw clean install
Luego, puedes ejecutar el JAR con el siguiente comando:
Visita http://localhost:8080 en tu navegador web. Deberías ver la aplicación Vaadin ejecutándose con un formulario para agregar, editar, filtrar y eliminar clientes.
Ejercicio
Crea una aplicación que con la base de datos de películas, permita mostrar películas buscando por el título en castellano.
Realiza una parte sólo de consulta con una versión reducida de la entidad Pelicula.
Usando CommandLineRunner: implementa la interfaz CommandLineRunner y sobreescribe el método run.
Ejemplo:
@SpringBootApplicationpublicclassMyApplicationimplements CommandLineRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Overridepublicvoidrun(String... args) {
// Aquí va el código de la aplicación }
}
Usando ApplicationRunner: implementa la interfaz ApplicationRunner y sobreescribe el método run.
Ejemplo:
@SpringBootApplicationpublicclassMyApplicationimplements ApplicationRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Overridepublicvoidrun(ApplicationArguments args) {
// Aquí va el código de la aplicación }
}
Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@ComponentpublicclassMiComponente {
@PostConstructpublicvoidinit() {
// Aquí va el código de la aplicación }
}
Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplicationpublicclassMyApplication {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Beanpublic CommandLineRunner run() {
return args -> {
// Aquí va el código de la aplicación };
}
}
Ejecutores de código al inicio de la Aplicación
Para la realización de una aplicación de consola en Spring Boot, es necesario crear un proyecto de Spring Boot y modificar la clase principal de la aplicación para que sea una aplicación de consola.
@SpringBootApplicationpublicclassMiAplicacionimplements ApplicationRunner {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
}
@Overridepublicvoidrun(ApplicationArguments args) {
// Aquí va el código de la aplicación }
}
Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@ComponentpublicclassMiComponente {
@PostConstructpublicvoidinit() {
// Aquí va el código de la aplicación }
}
@Component es una anotación que marca una clase como un componente de Spring. Spring escaneará las clases anotadas con @Component y las registrará en el contexto de la aplicación.
@PostConstruct es una anotación que se utiliza en un método que debe ejecutarse después de que se haya completado la construcción de un bean. Spring ejecutará el método anotado con @PostConstruct después de que se haya creado el bean.
Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplicationpublicclassMiAplicacion {
publicstaticvoidmain(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
}
@Beanpublic CommandLineRunner run() {
return args -> {
// Aquí va el código de la aplicación };
}
}
@Bean es una anotación que marca un método como un productor de un bean administrado por Spring. Spring llamará al método anotado con @Bean para crear el bean y lo registrará en el contexto de la aplicación.
Diferencias entre ejecutores de código al inicio de la Aplicación
Estos ejecutores se utilizan para ejecutar la lógica al iniciar la aplicación:
ApplicationRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.
ApplicationRunnerrecoge ApplicationArguments, que tiene métodos como getOptionNames(), getOptionValues() y getSourceArgs().
CommandLineRunner también es una Interfaz Funcional con el método run.
CommandLineRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.
Acepta los argumentos como un array de String que se pasan en el momento del inicio del servidor.
Ambos proporcionan la misma funcionalidad y la única diferencia entre CommandLineRunner y ApplicationRunner es que CommandLineRunner.run() acepta un array de String[], mientras que ApplicationRunner.run() acepta ApplicationArguments como argumento.
Hasta ahora, hemos visto de manera global Spring Boot, pero no hemos profundizado en todos sus componentes.
En este apartado hablaremos de los componentes más importantes de Spring Boot, que son los servicios, componentes y repositorios, así como las diferencias entre ellos.
Los tres componentes esenciales de una aplicación Spring Boot son:
Servicios.
Componentes
Repositorios, de los que ya hemos hablado y que son una parte fundamental de la arquitectura de Spring Boot para acceder a datos.
Servicios
Un servicio es un concepto fundamental en las aplicaciones Spring Boot. Representa una capa de la aplicación responsable de ejecutar la lógica de negocio y encapsular la funcionalidad de la aplicación. Generalmente, los servicios son sin estado y están diseñados para realizar tareas específicas.
En el ejemplo anterior, ServicioProducto encapsula la lógica de negocio relacionada con los productos. Interactúa con un RepositorioProducto para realizar operaciones CRUD.
Componentes
Componente es un término amplio que incluye tanto la anotación @Component como sus variantes especializadas como @Service, @Controller y @Repository. Todas estas anotaciones son especializaciones de @Component y se utilizan para definir beans de Spring.
@ComponentpublicclassEnviadorEmail {
publicvoidenviarEmail(String destinatario, String asunto, String cuerpo) {
// Lógica para enviar el correo electrónico// ... System.out.println("Correo enviado a "+ destinatario);
}
}
Aquí, EnviadorEmail es un componente sencillo, encargado de enviar correos electrónicos. Puede ser inyectado en otros componentes de Spring.
Repositorios
El repositorio, que ya hemos estudiado, pero que es conveniente declarar, es un concepto en Spring que simplifica el acceso a datos. Generalmente, se utiliza para interactuar con bases de datos.
Como sabes existen muchos tipos de repositorios, pero el más interesante para nosotros ahora es Spring Data JPA, que proporciona una manera poderosa y sencilla de trabajar con datos, generando automáticamente el código repetitivo necesario.
@RepositorypublicinterfaceRepositorioProductoextends JpaRepository<Producto, Long> {
List<Producto>findByCategoria(String categoria);
// Se pueden definir consultas personalizadas adicionales}
RepositorioProducto extiende JpaRepository y hereda varios métodos para operaciones comunes con la base de datos. Además, se pueden definir consultas personalizadas para un acceso más flexible a los datos.
Integración de Servicios, Componentes y Repositorios
@ServicepublicclassServicioPedido {
@Autowired// inyección de dependenciasprivate ServicioProducto servicioProducto; // El servicio encapsula la lógica de negocio y la comunicación con el repositorio@Autowiredprivate EnviadorEmail enviadorEmail;
publicvoidprocesarPedido(Long idProducto, String emailUsuario) {
Producto producto = servicioProducto.obtenerProductoPorId(idProducto);
// Lógica de negocio para procesar el pedido// ... enviadorEmail.enviarEmail(
emailUsuario,
"Confirmación de Pedido",
"¡Tu pedido ha sido procesado exitosamente!" );
}
}
En este ejemplo, ServicioPedido utiliza tanto ServicioProducto como EnviadorEmail para realizar el procesamiento del pedido. Cada componente cumple un rol específico, y su colaboración da como resultado una aplicación coherente y bien estructurada.
Comprender los roles de Servicios, Componentes y Repositorios es fundamental para construir aplicaciones Spring Boot mantenibles y escalables.
En este apartado vamos a profundizar en qué es un servicio en el contexto de Spring Boot y Spring Data JPA, sus características principales, cómo se crea, y terminaremos con un ejemplo completo y funcional que cubre desde la entidad hasta el uso del servicio.
Servicio en Spring
En Spring, un servicio es una clase de la capa de negocio que contiene la lógica principal de la aplicación.
Actúa como un puente entre el controlador (que gestiona la entrada del usuario) y el repositorio (que gestiona el acceso a datos).
Características
Está anotado con @Service, lo que lo convierte en un Spring Bean.
Contiene lógica de negocio: reglas, validaciones, cálculos.
Es reutilizable y modular.
No gestiona directamente las solicitudes HTTP (eso lo hace el controlador).
Utiliza los repositorios para acceder a la base de datos.
Suele ser inmutable y sin estado (stateless).
Creación
Crear la entidad (@Entity). Ya visto.
Debe tener un constructor vacío y otro con todos los atributos.
Debe tener un @Id.
Debe tener getters y setters para todos los atributos.
Debe tener un método toString() para facilitar la depuración.
Crear un repositorio (extends JpaRepository u otro tipo de repositorio).
Debe estar anotado con @Repository.
Debe extender de JpaRepository o de otro tipo de repositorio (CrudRepository, PagingAndSortingRepository, etc.).
Debe contener métodos para acceder a los datos (CRUD).
Puede contener métodos personalizados para consultas específicas.
Crear el servicio (@Service), inyectando el repositorio.
Crear un controlador (@RestController) para gestionar las solicitudes HTTP.
Debe estar anotado con @RestController.
Debe contener métodos para gestionar las solicitudes HTTP (GET, POST, PUT, DELETE).
Debe inyectar el servicio.
Usar el servicio desde un controlador o desde otro servicio.
Ejercicio
En este ejemplo vamos a crear una API REST para gestionar productos. La API tendrá las siguientes funcionalidades:
Listar todos los productos.
Buscar un producto por ID.
Buscar productos por categoría.
Crear un nuevo producto.
Eliminar un producto.
Realiza el ejercicio, siguiendo los pasos y creando los ficheros necesarios para completar la API REST.
Si estás usando Maven, las dependencias del archivo pom.xml serían (aproximadamente) las siguientes:
<dependencies><!-- Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- JPA --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- PostgreSQL --><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency><!-- Pruebas unitarias --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
Configuración application.properties
Crea este archivo en src/main/resources/application.properties:
# Configuración básica de conexiónspring.datasource.url=jdbc:postgresql://localhost:5432/tiendaspring.datasource.username=usuariospring.datasource.password=contraseña# Configuración de JPAspring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=truespring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect# Añade la estrategia de nombrado para evitar problemas con mayúsculasspring.jpa.properties.hibernate.globally_quoted_identifiers=truespring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl# Puerto del servidorserver.port=8080
Asegúrate de que tienes una base de datos llamada tienda creada en PostgreSQL y un usuario con permisos.
Si aún no has creado tu base de datos, puedes hacerlo desde la terminal:
obtenerTodos() devuelve todos los productos simulados.
guardar() comprueba que se guarda correctamente.
obtenerPorId() devuelve un producto si existe o null si no.
Todo se prueba sin una base de datos real, gracias a Mockito.
11. Proyecto Spring. Cuestionarios.
Crearemos un proyecto de gestión de preguntas, empezando por el modelo de datos, los DTO, los repositorios y los servicios.
Debes asegurar de completar aquello que sea necesario, en especial los métodos toString, hashCode o equals, entre otros. Es sólo un esqueleto, casi funcional, pero incompleto.
Es una aplicación para la gestión de Cuestionarios.
Existen dos tipos de Preguntas:
Cuestiones.
Pregunta tipo Test.
Aplicación
La aplicación es una API REST que permite gestionar preguntas y cuestionarios. La aplicación está dividida en varias capas:
Modelo: Contiene las entidades que representan los datos de la aplicación.
DTO: Contiene los objetos de transferencia de datos que se utilizan para la comunicación entre la API y el cliente.
Repositorios: Contiene las interfaces que permiten acceder a los datos de la aplicación.
Servicios: Contiene la lógica de negocio de la aplicación. Opcional. En este ejemplo no lo usamos, pero es una buena recomendación.
Controladores: Contiene las clases que manejan las peticiones HTTP y devuelven las respuestas al cliente.
package com.example.apppreguntas;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO;
/**
* Clase principal que inicia la aplicación Spring Boot para el sistema de gestión de preguntas.
*
* Esta clase configura los componentes esenciales de la aplicación y habilita soporte
* para integración entre Spring Data y Spring Web.
*/@SpringBootApplication// Anotación compuesta que incluye:// - @Configuration: Marca la clase como fuente de definiciones de beans// - @EnableAutoConfiguration: Habilita la configuración automática de Spring Boot// - @ComponentScan: Habilita el escaneo de componentes en el paquete actual y subpaquetes@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) // Habilita soporte para integración entre Spring Data y Spring Web// VIA_DTO indica que la paginación se serializará mediante DTOspublicclassAppPreguntasApplication {
/**
* Método principal que inicia la aplicación Spring Boot.
*
* @param args Argumentos de línea de comandos (opcionales)
*/publicstaticvoidmain(String[] args) {
SpringApplication.run(AppPreguntasApplication.class, args);
}
}
Fichero de propiedades
Se debe trabajar con PostgreSQL, por lo que el fichero de propiedades es el siguiente (ojo, está en modo update, por lo que no es adecuado para producción):
Es una anotación compuesta que combina tres anotaciones fundamentales:
@Configuration: Identifica la clase como una fuente de definiciones de beans para el contexto de la aplicación.
@EnableAutoConfiguration: Habilita la configuración automática de Spring Boot, que configura automáticamente los beans que detecta en el classpath.
@ComponentScan: Habilita el escaneo de componentes en el paquete actual y sus subpaquetes, buscando clases anotadas con @Component, @Service, @Repository, etc.
@EnableSpringDataWebSupport:
Habilita la integración entre Spring Data y Spring Web MVC.
Proporciona soporte para:
Conversión automática de parámetros de petición en objetos Pageable y Sort.
Soporte para la serialización de tipos de dominio como Point y Distance.
Registro de un LinkCollector para descubrir enlaces en las respuestas de Spring HATEOAS.
El parámetro pageSerializationMode = VIA_DTO:
Especifica cómo se deben serializar las páginas cuando se devuelven como parte de una respuesta REST.
VIA_DTO indica que las páginas deben serializarse mediante DTOs (Data Transfer Objects) en lugar de la implementación directa de Page.
Esto es más seguro y evita exponer detalles internos de implementación.
SpringApplication.run():
Método estático que inicia la aplicación Spring Boot.
Crea el ApplicationContext adecuado (basado en el classpath).
Registra los beans definidos.
Inicia el servidor web embebido (si está en el classpath).
Así disponemos de:
Configuración simplificada: @SpringBootApplication reduce la configuración manual necesaria.
Integración web y datos: @EnableSpringDataWebSupport facilita el trabajo con paginación y ordenación en los controladores REST.
Seguridad en serialización: El modo VIA_DTO protege contra la exposición accidental de información interna.
Escaneo automático: Se detectan automáticamente todos los componentes, repositorios y controladores.
Esta configuración es ideal para aplicaciones REST que utilizan Spring Data JPA y necesitan soporte avanzado para paginación y ordenación en sus endpoints.
Subsecciones de 11. Proyecto Spring. Cuestionarios.
El modelo de datos debe estar almacenado en una base de datos relacional. En este caso, se ha optado por usar PostgreSQL como motor de base de datos, tal y como hemos hecho en los últimos ejercicios.
La configuración de la base de datos se encuentra en el archivo application.properties del proyecto que ya hemos hecho otras veces.
Importante
Las clases indicadas están incompletas. En este ejercicio solo se muestran los atributos y las relaciones entre las entidades.
Usuario
Debes añadir las relaciones y aquello que consideredes necesario.
Los DTOs (Data Transfer Objects) son objetos que se utilizan para transferir datos entre diferentes capas de una aplicación, como la capa de presentación y la capa de negocio.
Los DTOs son útiles para encapsular datos y evitar la exposición directa de las entidades del modelo de dominio. Aquí los emplearemos para interactuar con la API REST.
Los DTOS (Data Transfer Objects) son objetos que se utilizan para transferir datos entre diferentes capas de una aplicación, como la capa de presentación y la capa de negocio:
Para recoger datos de la API REST.
Para reproducir el modelo para la respuesta
No es recomendable usar entidades para entrada y salida de datos, ya que pueden contener lógica de negocio y no son adecuadas para la serialización/deserialización.
Mapeo se emplea para Transformar un objeto de un tipo a otro.
De RequestDTO a Entity.
De Entity a ResponseDTO.
Para familiarizarte con el uso de DTOs, aquí tienes algunos ejemplos de DTOs que puedes usar en tu proyecto. Además, he empleado “record” que son data classes de Java que permiten crear objetos inmutables de forma sencilla con un constructor, getters y métodos equals y hashCode automáticamente.
OpcionResponse
package com.javhoz.ad.cuestionario.preguntas.dto;
import com.javhoz.ad.cuestionario.preguntas.model.Opcion;
/**
* DTO para representar una opción de una pregunta de tipo test en las respuestas API
* Contiene información básica de la opción: id, texto y si es correcta
*/publicrecordOpcionResponse(
Long idOpcion,
String texto,
boolean correcta) {
/**
* Método factory para crear un OpcionResponse a partir de una entidad Opcion
* @param opcion Entidad Opcion del modelo
* @return DTO OpcionResponse
*/publicstatic OpcionResponse of(Opcion opcion) {
returnnew OpcionResponse(
opcion.getIdOpcion(),
opcion.getTexto(),
opcion.isCorrecta());
}
}
EditarCuestionRequest
package com.javhoz.ad.cuestionario.preguntas.dto;
/**
* DTO para la edición de una pregunta básica (Cuestion)
* Contiene los campos editables: enunciado y descripción
* Se usa en las operaciones de actualización (PUT/PATCH)
*/publicrecordEditCuestionRequest(
String enunciado,
String descripcion) {
}
PreguntaRequest
package com.javhoz.ad.cuestionario.preguntas.dto;
import java.util.List;
/**
* DTO para la creación de nuevas preguntas
* Puede representar tanto preguntas básicas (Cuestion) como de tipo test (TipoTest)
*
* @param enunciado Texto principal de la pregunta
* @param descripcion Descripción detallada (para Cuestion)
* @param opciones Lista de opciones (para TipoTest)
* @param username Nombre de usuario del creador
*/publicrecordPreguntaRequest(
String enunciado,
String descripcion,
List<String> opciones,
String username) {
}
PreguntaResponse
package com.javhoz.ad.cuestionario.preguntas.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.javhoz.ad.cuestionario.preguntas.model.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* DTO principal para representar preguntas en las respuestas API
* Usa JsonInclude para omitir campos nulos en la respuesta JSON
*/@JsonInclude(JsonInclude.Include.NON_NULL)
publicrecordPreguntaResponse(
Long idPregunta,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern ="dd/MM/yyyy HH:mm:ss")
LocalDateTime fechaCreacion,
String enunciado,
String descripcion,
List<OpcionResponse> opciones,
String etiquetas) {
// Constructor alternativo para preguntas básicas (Cuestion)publicPreguntaResponse(Long idPregunta, LocalDateTime fechaCreacion, String enunciado, String descripcion, String etiquetas) {
this(idPregunta, fechaCreacion, enunciado, descripcion, null, etiquetas);
}
// Constructor alternativo para preguntas de tipo test (TipoTest)publicPreguntaResponse(Long idPregunta, LocalDateTime fechaCreacion, String enunciado, List<OpcionResponse> opciones, String etiquetas) {
this(idPregunta, fechaCreacion, enunciado, null, opciones, etiquetas);
}
/**
* Factory method para crear respuesta a partir de una Cuestion
* @param cuestion Pregunta básica
* @return DTO PreguntaResponse
*/publicstatic PreguntaResponse of(Cuestion cuestion) {
returnnew PreguntaResponse(
cuestion.getIdPregunta(),
cuestion.getFechaCreacion(),
cuestion.getEnunciado(),
cuestion.getDescripcion(),
cuestion.getEtiquetas().isEmpty() ?null :
cuestion.getEtiquetas()
.stream()
.map(Etiqueta::getNombre)
.collect(Collectors.joining(", ")));
}
/**
* Factory method para crear respuesta a partir de un TipoTest
* @param tipoTest Pregunta de tipo test
* @return DTO PreguntaResponse
*/publicstatic PreguntaResponse of(TipoTest tipoTest) {
returnnew PreguntaResponse(
tipoTest.getIdPregunta(),
tipoTest.getFechaCreacion(),
tipoTest.getEnunciado(),
tipoTest.getOpciones()
.stream()
.map(OpcionResponse::of)
.toList(),
tipoTest.getEtiquetas().isEmpty() ?null :
tipoTest.getEtiquetas()
.stream()
.map(Etiqueta::getNombre)
.collect(Collectors.joining(", ")));
}
/**
* Factory method genérico que decide qué tipo de respuesta crear
* según el tipo de pregunta recibida
* @param pregunta Pregunta (puede ser Cuestion o TipoTest)
* @return DTO PreguntaResponse adecuado
*/publicstatic PreguntaResponse of(Pregunta pregunta) {
if (pregunta instanceof Cuestion cuestion)
return PreguntaResponse.of(cuestion);
return PreguntaResponse.of((TipoTest) pregunta);
}
}
Hemos trabajado bastante ya con repositorios, pero resulta importante ver cómo se pueden integrar con consultas personalizadas, con la paginación o parametrización de las mismas.
Tenemos tres repositorios, uno de ellos es polimórfico, Pregunta, y los otros dos son específicos para las entidades Etiqueta y Usuario.
Para petición POST de una nueva cuestión emplearemos PreguntaRequest, que es un DTO (Data Transfer Object) que contiene los datos necesarios para crear una nueva pregunta. Devolveremos 201 Created y la nueva pregunta en el cuerpo de la respuesta.
Para petición GET de una cuestión por ID emplearemos PreguntaResponse, que es un DTO que contiene los datos de la pregunta, incluyendo sus opciones y etiquetas.
Debes completar los métodos de los repositorios, en especial el de PreguntaRepository que es el más complejo.
PreguntaRepository
package com.javhoz.ad.cuestionario.preguntas.repositories;
import com.javhoz.ad.cuestionario.preguntas.model.Pregunta;
import jakarta.transaction.Transactional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
publicinterfacePreguntaRepositoryextends JpaRepository<Pregunta, Long> {
/**
* findAllWithOpcionesAndEtiquetas
* Busca todas las preguntas con sus relaciones (opciones y etiquetas) cargadas
* @param pageable Configuración de paginación
* @return Página de preguntas con relaciones cargadas
*//**
* findByIdWithOpcionesAndEtiquetas
* Busca una pregunta por ID con sus relaciones (opciones y etiquetas) cargadas
* @param id ID de la pregunta
* @return Optional con la pregunta si existe
*//**
* existsByIdAndPreguntaType
* Verifica si existe una pregunta de un tipo específico por ID
* @param preguntaType Tipo de pregunta (Cuestion.class o TipoTest.class)
* @param id ID de la pregunta
* @return true si existe, false en caso contrario
*//**
* toggleOpcionCorrecta
* Emplea un query nativo para cambiar el estado de "correcta" de una opción (toggle)
* Cambia el estado de "correcta" de una opción (toggle)
* @param idPregunta ID de la pregunta de tipo test
* @param idOpcion ID de la opción a modificar
* @return Número de registros afectados
*/@Modifying@Transactional@Query(value ="""
Tu consulta SQL aquí
""", nativeQuery =true)
inttoggleOpcionCorrecta(Long idPregunta, Long idOpcion);
}
EtiquetaRepository
package com.javhoz.ad.cuestionario.preguntas.repositories;
import com.javhoz.ad.cuestionario.preguntas.model.Etiqueta;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
publicinterfaceEtiquetaRepositoryextends JpaRepository<Etiqueta, Long> {
/**
* Busca una etiqueta por su nombre
* @param nombre Nombre de la etiqueta
* @return Optional con la etiqueta si existe
*/}
UsuarioRepository
package com.javhoz.ad.cuestionario.preguntas.repositories;
import com.javhoz.ad.cuestionario.preguntas.model.Usuario;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
publicinterfaceUsuarioRepositoryextends JpaRepository<Usuario, Long> {
/**
* Busca un usuario por su nombre de login
* @param login Nombre de usuario
* @return Optional con el usuario si existe
*/}
04. Controller
El controller es la capa que se encarga de recibir las peticiones HTTP y devolver las respuestas correspondientes. En este caso, el controlador PreguntaController maneja las operaciones relacionadas con las preguntas, como crear, editar, eliminar y obtener preguntas.
Este controlador debe funcionar con el modelo implementado.
PreguntaController
package com.javhoz.ad.cuestionario.preguntas.controller;
import com.javhoz.ad.cuestionario.preguntas.dto.EditCuestionRequest;
import com.javhoz.ad.cuestionario.preguntas.dto.PreguntaRequest;
import com.javhoz.ad.cuestionario.preguntas.dto.PreguntaResponse;
import com.javhoz.ad.cuestionario.preguntas.model.*;
import com.javhoz.ad.cuestionario.preguntas.repositories.EtiquetaRepository;
import com.javhoz.ad.cuestionario.preguntas.repositories.PreguntaRepository;
import com.javhoz.ad.cuestionario.preguntas.repositories.UsuarioRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.Optional;
@RestController@RequestMapping("/pregunta/")
publicclassPreguntaController {
privatefinal PreguntaRepository preguntaRepository;
privatefinal UsuarioRepository usuarioRepository;
privatefinal EtiquetaRepository etiquetaRepository;
publicPreguntaController(PreguntaRepository preguntaRepository,
UsuarioRepository usuarioRepository,
EtiquetaRepository etiquetaRepository) {
this.preguntaRepository= preguntaRepository;
this.usuarioRepository= usuarioRepository;
this.etiquetaRepository= etiquetaRepository;
}
@PostMapping("/new/cuestion")
public ResponseEntity<PreguntaResponse>newCuestion(@RequestBody PreguntaRequest preguntaRequest) {
Optional<Usuario> propietario = usuarioRepository.findByLogin(preguntaRequest.username());
Cuestion cuestion =new Cuestion();
cuestion.setAutor(propietario.orElse(null));
cuestion.setEnunciado(preguntaRequest.enunciado() !=null? preguntaRequest.enunciado() : "Sin enunciado");
cuestion.setDescripcion(preguntaRequest.descripcion());
cuestion = preguntaRepository.save(cuestion);
URI uri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/pregunta/{id}")
.build(cuestion.getIdPregunta());
return ResponseEntity.created(uri).body(PreguntaResponse.of(cuestion));
}
@PostMapping("/new/tipotest")
public ResponseEntity<PreguntaResponse>newTipoTest(@RequestBody PreguntaRequest preguntaRequest) {
Optional<Usuario> propietario = usuarioRepository.findByLogin(preguntaRequest.username());
TipoTest tipoTest =new TipoTest();
tipoTest.setAutor(propietario.orElse(null));
tipoTest.setEnunciado(preguntaRequest.enunciado() !=null? preguntaRequest.enunciado() : "Sin enunciado");
preguntaRequest.opciones().stream()
.map(texto -> {
Opcion opcion =new Opcion();
opcion.setTexto(texto);
return opcion;
})
.forEach(tipoTest::addOpcion);
tipoTest = preguntaRepository.save(tipoTest);
URI uri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/pregunta/{id}")
.build(tipoTest.getIdPregunta());
return ResponseEntity.created(uri).body(PreguntaResponse.of(tipoTest));
}
@GetMapping("/")
public Page<PreguntaResponse>getAll(@PageableDefault(page=0, size=5, sort ="fechaCreacion") Pageable pageable) {
Page<Pregunta> result = preguntaRepository.findAllWithOpcionesAndEtiquetas(pageable);
if (result.isEmpty())
thrownew ResponseStatusException(HttpStatus.NOT_FOUND, "No se encontraron preguntas");
return result.map(PreguntaResponse::of);
}
@GetMapping("/{id}")
public PreguntaResponse getById(@PathVariable Long id) {
return preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
.map(PreguntaResponse::of)
.orElseThrow(() ->new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Pregunta con ID %d no encontrada".formatted(id)));
}
@PutMapping("/cuestion/{id}")
public ResponseEntity<PreguntaResponse>editCuestion(
@RequestBody EditCuestionRequest editCuestionRequest,
@PathVariable Long id) {
if (!preguntaRepository.existsByIdAndPreguntaType(Cuestion.class, id)) {
thrownew ResponseStatusException(
HttpStatus.NOT_FOUND,
"Pregunta con ID %d no encontrada".formatted(id));
}
return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
.map(Cuestion.class::cast)
.map(cuestion -> {
cuestion.setEnunciado(editCuestionRequest.enunciado());
cuestion.setDescripcion(editCuestionRequest.descripcion());
return preguntaRepository.save(cuestion);
})
.map(PreguntaResponse::of));
}
@PutMapping("/tipotest/{id}/add/{opcion}")
public ResponseEntity<PreguntaResponse>addOpcionToTipoTest(
@PathVariable String opcion,
@PathVariable Long id) {
if (!preguntaRepository.existsByIdAndPreguntaType(TipoTest.class, id)) {
thrownew ResponseStatusException(
HttpStatus.NOT_FOUND,
"Pregunta con ID %d no encontrada".formatted(id));
}
return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
.map(TipoTest.class::cast)
.map(tipoTest -> {
Opcion nuevaOpcion =new Opcion();
nuevaOpcion.setTexto(opcion);
tipoTest.addOpcion(nuevaOpcion);
return preguntaRepository.save(tipoTest);
})
.map(PreguntaResponse::of));
}
@DeleteMapping("/tipotest/{id}/del/{opcion_id}")
public ResponseEntity<PreguntaResponse>deleteOpcionFromTipoTest(
@PathVariable("opcion_id") Long opcionId,
@PathVariable Long id) {
if (!preguntaRepository.existsByIdAndPreguntaType(TipoTest.class, id)) {
thrownew ResponseStatusException(
HttpStatus.NOT_FOUND,
"Pregunta con ID %d no encontrada".formatted(id));
}
return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
.map(TipoTest.class::cast)
.map(tipoTest -> {
tipoTest.removeOpcionById(opcionId);
return preguntaRepository.save(tipoTest);
})
.map(PreguntaResponse::of));
}
@PutMapping("/tipotest/{id}/toggle/{opcion_id}")
public ResponseEntity<PreguntaResponse>toggleOpcionCorrecta(
@PathVariable("opcion_id") Long opcionId,
@PathVariable Long id) {
if (!preguntaRepository.existsByIdAndPreguntaType(TipoTest.class, id)) {
thrownew ResponseStatusException(
HttpStatus.NOT_FOUND,
"Pregunta con ID %d no encontrada".formatted(id));
}
preguntaRepository.toggleOpcionCorrecta(id, opcionId);
return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
.map(PreguntaResponse::of));
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deletePregunta(@PathVariable Long id) {
preguntaRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
@PutMapping("/{id}/etiqueta/add/{etiqueta}")
public ResponseEntity<PreguntaResponse>addEtiqueta(
@PathVariable Long id,
@PathVariable String etiqueta) {
Etiqueta nuevaEtiqueta = etiquetaRepository.findByNombre(etiqueta)
.orElseGet(() -> {
Etiqueta e =new Etiqueta();
e.setNombre(etiqueta);
return etiquetaRepository.save(e);
});
return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
.map(pregunta -> {
pregunta.getEtiquetas().add(nuevaEtiqueta);
return preguntaRepository.save(pregunta);
})
.map(PreguntaResponse::of));
}
@DeleteMapping("/{id}/etiqueta/del/{etiqueta}")
public ResponseEntity<PreguntaResponse>deleteEtiqueta(
@PathVariable Long id,
@PathVariable String etiqueta) {
Optional<Etiqueta> etiquetaExistente = etiquetaRepository.findByNombre(etiqueta);
if (etiquetaExistente.isPresent()) {
return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
.map(pregunta -> {
pregunta.getEtiquetas().removeIf(e -> e.getNombre().equalsIgnoreCase(etiqueta));
return preguntaRepository.save(pregunta);
})
.map(PreguntaResponse::of));
}
return ResponseEntity.notFound().build();
}
}
Este proyecto proporciona un API REST completo para gestionar preguntas, opciones y etiquetas. A continuación, te mostraré cómo interactuar con los endpoints usando Postman:
{
"enunciado": "¿Qué es Spring Boot?",
"descripcion": "Explica brevemente qué es Spring Boot.",
"username": "javhoz"}
✅ Respuesta Esperada (201 Created):
{
"idPregunta": 1,
"fechaCreacion": "2025-05-20T10:00:00",
"enunciado": "¿Qué es Spring Boot?",
"descripcion": "Explica brevemente qué es Spring Boot."}
En el proyecto debes incluir capturas de pantalla de las respuestas de Postman para cada uno de los endpoints.
Añadir Capa de Servicio
Actualmente, el controlador gestiona lógica de negocio, lo que no es ideal. Es preciso extraerla a una capa de servicio para mejorar la estructura.
Debes implantar el proyecto con una capa de servicio que gestione la lógica de negocio: PreguntaService.
Beneficios de emplear una capa de servicio:
Separación de responsabilidades: El controlador solo maneja HTTP.
Código más mantenible: La lógica de negocio está centralizada.
Más fácil de testear: Se pueden mockear servicios en pruebas unitarias.
Resumen
🔹 Usa Postman para probar el API fácilmente.
🔹 Refactoriza con una capa de servicio para mejorar la estructura del código.
🔹 Añade más funcionalidades:
Práctica: API REST para Gestión de Preguntas y Cuestionarios
Objetivo
Se trata de desarrollar una API REST con Spring Boot que permita gestionar preguntas (básicas y tipo test) con opciones, etiquetas y usuarios, tal y como hemos visto en la práctica anterior. Además, se valorará la implementación opcional de una interfaz web con Thymeleaf para interactuar con el sistema.
A. Parte Obligatoria (API REST)
1. Modelo de Datos (40%)
El modelo de datos sigue la estructura de la práctica anterior, pero con algunas mejoras y adaptaciones para el uso de Spring Data JPA, ya que no se han implementado las relaciones, que deben estar correctamente definidas.
La herencia debe estar perfectamente implementada, y las relaciones deben ser bidireccionales donde sea necesario.
Código fuente GitHub y ZIP subido en el aula virtual.
Opcional:
Capturas de la interfaz web funcionando.
Postman collection o Swagger (opcional)
Ejemplos
1. Ejemplo de Petición (Postman)
Creaación de pregunta tipo test:
POST /pregunta/new/tipotest
Body:
{
"enunciado": "Capitales de Europa",
"opciones": ["Madrid", "Lisboa", "Berlín"],
"username": "profesor1"
}
Solución Propuesta
Repositorio:
publicinterfacePreguntaRepositoryextends JpaRepository<Pregunta, Long> {
@Query("SELECT p FROM Pregunta p LEFT JOIN FETCH p.opciones WHERE p.id = :id")
Optional<Pregunta>findByIdWithOpciones(Long id);
}
Servicio:
@ServicepublicclassPreguntaService {
public Pregunta crearTipoTest(PreguntaRequest request) {
Usuario usuario = usuarioRepository.findByLogin(request.username())
.orElseThrow(() ->new UsuarioNoEncontradoException(request.username()));
// Lógica de creación... }
}
La anotación @Modifying se usa en Spring Data JPA para indicar que una consulta JPQL o SQL modificará datos (INSERT, UPDATE, DELETE). Sin ella, las consultas personalizadas con @Query son de solo lectura (read-only).
Uso:
En consultas que actualicen (UPDATE) o eliminen (DELETE) registros.
Ejemplo típico: Cambiar el estado de un campo (ej: marcar una opción como correcta).
Ejemplo:
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
publicinterfacePreguntaRepositoryextends JpaRepository<Pregunta, Long> {
@Modifying// Indica que la consulta MODIFICA datos@Query("UPDATE Opcion o SET o.correcta = NOT o.correcta WHERE o.id = :opcionId")
voidtoggleOpcionCorrecta(@Param("opcionId") Long opcionId);
}
Requiere @Transactional (en el servicio o método).
No retorna entidades, sino un void o el número de registros afectados (int).
3. Ejemplo Básico de Plantilla Thymeleaf
Para hacerlo necesitamos un controlador y una plantilla HTML que muestre datos dinámicos, itere sobre una lista y maneje formularios.
Thymeleaf
Thymeleaf es un motor de plantillas para Java que permite crear vistas dinámicas en aplicaciones web. Se integra fácilmente con Spring Boot y ofrece una sintaxis intuitiva para trabajar con datos del modelo.
Elementos clave de Thymeleaf:
Atributos de Thymeleaf: Se usan prefijos como th: para indicar que son atributos especiales.
Ejemplo: th:text, th:each, th:href.
Expresiones de Thymeleaf: Se utilizan para acceder a variables del modelo, iterar sobre colecciones y generar URLs dinámicas:
Ejemplo: ${variable}, @{/ruta}.
Condicionales y bucles: Permite mostrar u ocultar contenido basado en condiciones o iterar sobre listas.
Ejemplo: th:if, th:each.
Controlador
A continuación muestro un controlador de ejemplo que maneja una lista de preguntas y un formulario para añadir nuevas preguntas.
@ControllerpublicclassPreguntaController {
@GetMapping("/preguntas")
public String listarPreguntas(Model model) {
model.addAttribute("titulo", "Listado de Preguntas");
model.addAttribute("preguntas", preguntaService.findAll());
model.addAttribute("preguntaRequest", new PreguntaRequest());
return"listado"; // Nombre de la plantilla Thymeleaf }
}
Plantilla Thymeleaf
<!DOCTYPE html><htmlxmlns:th="http://www.thymeleaf.org"> <!-- Namespace de Thymeleaf --><head>
<metacharset="UTF-8">
<title>Listado de Preguntas</title>
</head>
<body>
<!-- 1. Mostrar datos dinámicos --> <h1th:text="${titulo}">Título por defecto</h1> <!-- th:text reemplaza el contenido --><!-- 2. Iterar sobre una lista --> <ulth:each="pregunta : ${preguntas}"> <!-- th:each como un for-each --> <li>
<spanth:text="${pregunta.enunciado}">Enunciado</span>
<ath:href="@{/preguntas/{id}(id=${pregunta.id})}">Ver detalle</a> <!-- th:href para URLs dinámicas --> </li>
</ul>
<!-- 3. Formulario para añadir preguntas --> <formth:action="@{/preguntas/nueva}"method="post"th:object="${preguntaRequest}">
<inputtype="text"th:field="*{enunciado}"placeholder="Enunciado">
<buttontype="submit">Crear</button>
</form>
</body>
</html>
Elementos de la plantilla:
Atributo
Función
th:text
Muestra texto dinámico (ej: "${variable}").
th:each
Itera sobre colecciones (como v-for en Vue o *ngFor en Angular).
th:href
Genera URLs dinámicas (ej: @{/ruta/{id}(id=${variable})}).
th:action
Define la acción del formulario (enlace al endpoint del controlador).
th:object
Vincula el formulario a un objeto DTO (th:field mapea los campos).
Este sencillo ejemplo muestra cómo integrar los conceptos anteriores en un proyecto Spring Boot con Thymeleaf y Spring Security en una aplicación de gestión de preguntas.
Para ello se precisa un Controller, una configuración de seguridad y una estructura de plantillas.
Controlador:
@ControllerpublicclassPreguntaController {
@GetMapping("/preguntas") // Ruta para listar preguntaspublic String listarPreguntas(Model model) {
model.addAttribute("titulo", "Listado de Preguntas"); // Este atributo se usará en la vista (template) model.addAttribute("preguntas", preguntaService.findAll()); // Añade la lista de preguntas al modeloreturn"listado"; // Renderiza listado.html }
}
Spring Security:
Configura rutas protegidas (/admin/**) y un formulario de login.
Carpeta de plantillas:
Las vistas deben estar en src/main/resources/templates/.
UD 5. Bases de datos no SQL. MongoDB
UD 4. Bases de datos no SQL. MongoDB
1. ¿Qué son las bases de datos NoSQL?
Durante un cuarto de siglo, las bases de datos relacionales (RDBMS) han sido el modelo dominante para la gestión de bases de datos. Pero, hoy en
día, las bases de datos no relacionales, “cloud” o “NoSQL” están ganando terreno como modelo alternativo para la gestión de bases de datos.
En el mundo de la tecnología de bases de datos, existen dos tipos principales de bases de datos:
SQL o bases de datos relacionales-
NoSQL,no relacionales (más adecuado: no sólo relacionales)
La diferencia radica en cómo están construidas, el tipo de información que almacenan y cómo la almacenan.
Las bases de datos relacionales están estructuradas, como las guías telefónicas que almacenan números de teléfono y direcciones. Las bases de datos no relacionales no almacenan datos de forma tabular y están orientadas los documentos, grafos, diccionarios… También suelen estar distribuidas y proporcionan esquemas flexibles y escalables con grandes cantidades de datos y cargas de usuarios.
Subsecciones de UD 5. Bases de datos no SQL. MongoDB
01. Bases de datos NoSQL.
1. Introducción
Las bases de datos desempeñan un papel importante en la informática, ya que son el mecanismo central para almacenar, organizar, gestionar y recuperar grandes cantidades de datos.
En cuanto a los sistemas de gestión de bases de datos, existen dos tipos básicos:
SQL.
NoSQL.
Si los requisitos de datos no están claros desde el principio o si estamos tratando con cantidades masivas de datos no estructurados, quizás no podamos permitirnos diseñar una base de datos relacional con un esquema claramente definido. En este caso, utilizar bases de datos no relacionales nos ofrece una mayor flexibilidad.
Podemos ver las bases de datos no relacionales como un conjunto de directorios que contienen ficheros, que almacenan información relacionada de todo tipo.
Por ejemplo, si un blog usase una base de datos NoSQL, se podría almacenar un fichero con la información de cada post: fotos,
texto, métricas, enlaces, etc.
El hecho de intentar almacenar, procesar y analizar datos no estructurados dio lugar al desarrollo de herramientas alternativas a SQL. Estas herramientas se conocen como NoSQL (Not only SQL).
2. Bases de Datos SQL
Las bases de datos SQL (Structured Query Language) proporcionan precisión de la información al mantener una fuerte consistencia de datos y admitir transacciones complejas. Son adecuadas para operaciones que requieren datos organizados y un alto nivel de integridad de datos debido a su enfoque unificado, que les permite trabajar con varios frameworks y plataformas:
Además, mantienen el modelo relacional, que divide la información en tablas con filas y columnas. Este tipo de base de datos crea un esquema predeterminado que explica la estructura de los datos y las relaciones.
Las bases de datos SQL consultan, alteran y gestionan datos utilizando el lenguaje SQL:
Son algunas de las bases de datos SQL más populares.
3. Características de las Bases de Datos SQL
El esquema es la característica inicial de una base de datos SQL.
Un esquema describe la estructura de la base de datos, incluyendo tablas, columnas, tipos de datos y relaciones entre tablas. Esta característica clave asegura la consistencia de los datos y permite búsquedas e indexación rápidas.
Las bases de datos SQL se benefician de la amplia implantación y soporte del lenguaje SQL. SQL permite a los desarrolladores utilizar una única sintaxis para construir consultas, realizar uniones complejas y modificar datos, junto con una gran cantidad de herramientas, bibliotecas y marcos que facilitan la gestión, informes y análisis de datos.
Las bases de datos SQL proporcionan las propiedades ACID (Atomicidad, Consistencia, Aislamiento y Durabilidad), que aseguran la integridad de los datos y la confiabilidad transaccional.
La atomicidad garantiza que una transacción se maneje como una unidad única, ya sea completamente completada o completamente revertida en caso de fallo.
La consistencia de la base de datos garantiza que esté en un estado legítimo tanto antes como después de una transacción.
El aislamiento confirma que las operaciones simultáneas no entran en conflicto entre sí.
La durabilidad asegura que una vez que se ha registrado una transacción, continuará existiendo incluso si el sistema falla.
4. Bases de Datos NoSQL
Las bases de datos NoSQL significan “no solo SQL”, indicando que estas bases de datos no se limitan al modelo relacional tradicional.
Estas bases de datos sobresalen en situaciones donde las estructuras de datos pueden ser variadas, y la capacidad para manejar datos dinámicos, no estructurados o semiestructurados es fundamental:
Una ventaja principal de las bases de datos NoSQL es su capacidad para crecer horizontalmente, lo que les permite manejar cantidades enormes de datos y cargas de tráfico pesadas de manera efectiva.
Logran esta escalabilidad distribuyendo datos en numerosos nodos de clúster, permitiendo el procesamiento simultáneo.
Las bases de datos NoSQL admiten la disponibilidad sobre la consistencia robusta, proporcionando lo que se podría llamar “consistencia eventual”. Esto implica que las modificaciones de la base de datos pueden tardar tiempo en propagarse a través de todos los nodos, aumentando la disponibilidad, confiabilidad y tolerancia a fallos.
Exiten muchos frameworks o utilidades para gestionar y analizar fácilmente diversos tipos de datos y expandir las aplicaciones para abordar cantidades crecientes de datos eligiendo un tipo de base de datos NoSQL adecuado.
5. Tipos de Bases de Datos NoSQL
Existen cuatro tipos principales de bases de datos NoSQL:
Orientadas a documentos.
Clave-valor,
Orientadas a columnas.
Orientadas a grafos.
5.1. Bases de Datos Orientadas a Documentos
Las bases de datos orientadas a documentos almacenan datos en documentos flexibles y autoencriptados (como JSON o XML) que pueden modificarse rápidamente sin afectar toda la base de datos.
Son apropiadas para manejo de estructuras de datos jerárquicas.
Están diseñadas para almacenar datos en documentos que pueden tener diferentes formas en función de la información a almacenar.
Representan las relaciones utilizando subdocumentos y/o arrays incrustados en un único documento.
El documento es análogo al objeto en la programación orientada a objetos y proporciona una representación clara y natural de una entidad del mundo real y sus datos. Esta clara representación provoca que no sea necesario realizar un mapeo entre la base de datos y la aplicación.
El documento permite, a menudo, representar de forma exacta el objeto que el programador desea utilizar. La flexibilidad del documento a la hora de almacenar información en múltiples formatos al mismo tiempo proporciona también una gran flexibilidad a la hora de realizar el modelado.
{width=“5.9in”
height=“3.0833333333333335in”}
El aspecto clave a comprender en este tipo de bases de datos es que los datos relacionados se almacenan de forma conjunta, pero no tienen por qué tener el mismo formato. Esto es, un documento puede estar relacionado con otro y estar almacenados de forma conjunta, pero no tienen por qué contener los mismos campos de datos.
5.2. Bases de Datos Clave-Valor
Los gestores de datos clave-valor almacenan datos como pares simple clave-valor, ofreciendo un rendimiento respetable para aplicaciones de alto rendimiento y se utilizan principalmente para aplicaciones web de alto tráfico.
El esquema es muy simple: una clave única está relacionada con una serie de valores, que pueden ser desde un string hasta un objeto binario.
La ventaja de este tipo de bases de datos es que las consultas son muy simples. El sistema sabe en qué servidor se localiza la información y envía la petición a ese servidor.
No es recomendable cuando nuestro esquema tiene relaciones complejas.
5.3. Bases de Datos Columnares
Otra base de datos NoSQL bastante popular es la base de datos columnar, que almacena datos en columnas en lugar de filas, lo que las hace adecuadas para cargas de trabajo analíticas que implican la búsqueda de atributos específicos en conjuntos de datos grandes.
Están diseñadas principalmente para análisis de datos.
La ventaja es que devuelven los datos en columnas, haciendo que las consultas sean mucho
más eficientes, de forma que no devuelvan datos inútiles.
La clave primaria en estas bases de datos es el dato/valor que después es mapeado a las claves de las filas. Esto es opuesto a una clave primaria en una base de datos relacional.
La estructura de los datos que están en las columnas es flexible y puede variar de fila a fila.
5.4. Bases de Datos Orientadas a Grafos
Las bases de datos de grafos se centran en el almacenamiento y la búsqueda de asociaciones entre entidades, lo que las hace adecuadas para redes complejas o redes sociales, motores de recomendación o detección de fraudes.
Están diseñadas para tratar con problemas de relaciones y se centran en la conexión de los datos. Tiene la ventaja de que permite representar relaciones complejas. Sin embargo, muchos problemas no se modelan así de forma natural.
La información se almacena como una colección de nodos y aristas, donde las aristas representan las relaciones entre los nodos. El hecho de almacenar las relaciones entre los datos permite que los datos relacionados se puedan recuperar en una sola operación.
6. Ejemplos de Bases de Datos NoSQL
Ejemplos típicos de bases de datos NoSQL incluyen:
MongoDB.
Cassandra.
Redis.
Neo4j.
7. Características de las Bases de Datos NoSQL
Esquema flexible, escalabilidad, consistencia eventual y casos de uso específicos son las características clave de las bases de datos NoSQL.
La característica diferencial de las bases de datos NoSQL es el esquema flexible, que permite la estructuración de datos flexible y dinámica.
Las bases de datos NoSQL ofrecen una variedad de estructuras, haciéndolas adecuadas cuando los modelos de datos están evolucionando o al tratar con datos no estructurados o semiestructurados. Esta flexibilidad elimina las costosas transiciones de esquema y permite la prototipación rápida y la reutilización.
La escalabilidad es otra característica crucial de las bases de datos NoSQL. Estas bases de datos están diseñadas para manejar grandes volúmenes de datos y cargas de tráfico elevadas, lo que las hace altamente escalables horizontalmente. Las bases de datos NoSQL distribuyen datos en múltiples nodos en un clúster y, como resultado, pueden acomodar mayores demandas de datos y usuarios, así como procesamiento paralelo.
La “consistencia eventual”, que significa preferir la disponibilidad y la tolerancia a particiones sobre una estricta consistencia de datos. Este compromiso permite una alta disponibilidad y tolerancia a fallos, ya que el sistema puede seguir operando incluso en presencia de particiones de red o fallos de nodos.
Las bases de datos NoSQL suelen estar diseñadas con casos de uso específicos en mente.
8. Bases de Datos SQL vs. NoSQL
Comprender las ventajas y desventajas de las bases de datos SQL y NoSQL es crucial, ya que satisfacen ciertas necesidades de aplicación.
8.1. ACID vs. BASE
Las bases de datos SQL o relacionales están focalizadas hacia la fiabilidad de las transacciones, modelo ACID (Atomicity, Consistency, Isolation, Durability).
Las bases de datos NoSQL es más relevante cuando la magnitud y dinamismo de los datos cobran importancia y el modelo ACID de los modelos
relacionales queda en segundo plano frente al rendimiento, disponibilidad y escalabilidad, que son características más propias de las bases de datos NoSQL o no relacionales.
Hoy en día, los sistemas de almacenamiento de datos en Internet se ajustan más al conocido como modelo BASE (Basic Availability, Soft state, Eventually consistency), aunque también hay bases de datos NoSQL compatibles con ACID. El modelo BASE permite obtener una flexibilidad máxima.
Sus principales diferencias se destacan en la siguiente tabla:
Horizontal (Aumento de capacidad mediante la adición de nodos)
Consistencia
ACID (Consistencia estricta)
Eventual (Consistencia eventual)
9. ¿Cuándo usar bases de datos NoSQL?
9.1. Aplicaciones de Bases de Datos SQL
Las aplicaciones que requieren una sólida integridad de datos, modelos de datos organizados y transacciones intrincadas son más adecuadas para las bases de datos SQL. Rendimiento excepcional en situaciones que involucran sistemas financieros, plataformas de comercio electrónico y aplicaciones corporativas convencionales donde la integridad y confiabilidad de los datos son cruciales.
9.2. Aplicaciones de Bases de Datos NoSQL
Por otro lado, las bases de datos NoSQL son una mejor opción para programas que necesitan alta escalabilidad y modelos de datos adaptables. Sobresalen en aplicaciones como:
Sistemas de gestión de contenido.
Análisis en tiempo real
Plataformas de redes sociales
En las cuales las estructuras de datos dinámicas incluyen grandes volúmenes de datos no estructurados o semiestructurados.
Las bases de datos NoSQL (sobre todo las documentales) son apropiadas para almacenar datos polimórficos que pueden cambiar frecuentemente.
Por ejemplo, el modelo documental permite almacenar en la misma colección datos con diferentes formas, lo que significa que los documentos pueden tener campos diferentes.
Se pueden modificar los campos de un documento sin preocuparnos por el impacto o “efectos secundarios” que esto pueda tener sobre la base de datos. Por ejemplo, la base de datos no necesita ser actualizada cuando necesitamos añadir un nuevo campo, algo que sí pasa con las bases de datos relacionales.
Otro aspecto a destacar de las bases de datos NoSQL es que nos permiten representar los objetos de una forma muy fiel y directa. Esto significa que podemos intercambiar datos entre la aplicación y la base de datos sin necesidad de mapearlos. Esto también mejora la productividad, ya que no necesitamos código para realizar traducciones entre la aplicación y la base de datos.
Los sistemas NoSQL típicamente son “cloud-native” y están diseñados de forma distribuida, lo que resulta interesante si necesitamos tener una base de datos fácilmente escalable.
La decisión entre bases de datos SQL y NoSQL, o un enfoque híbrido que combina las fortalezas de ambas, depende en última instancia de los requisitos específicos de la aplicación y los datos. La consistencia de datos, las necesidades de escalabilidad, la velocidad de desarrollo, la flexibilidad y el soporte del ecosistema deben ser considerados.
03. Conexión a MongoDB con String Data MongoDB (práctica).
En esta práctica trabajaremos con un proyecto Spring Data MongoDB para construir una aplicación que almacena datos en MongoDB.
Almacenaremos objetos UsuarioPOJO (Plain Old Java Objects) en una base de datos MongoDB utilizando Spring Data MongoDB.
Requisitos:
Conocimientos de Java. ;-)
IDE.
Java 17 o posterior.
Maven 3.5+.
2. Spring Initializr
Para crear el proyecto:
Ve a Spring Initializr. Este servicio incorpora todas las dependencias que necesitas para una aplicación y realiza la mayor parte de la configuración.
Elige Maven y el lenguaje Java.
Haz clic en Dependencies y selecciona Spring Data MongoDB.
Haz clic en Generate.
Descarga el archivo ZIP resultante, que es un archivo comprimido de una aplicación web.
Como hemos visto, IntellJ Ultimate, Eclipse o Visual Studio Code, disponen de un Spring Initializr que permite crearlo sin acceder a la página Web de Spring Boot.
3. Instalación e inicio de MongoDB
Con tu proyecto configurado, puedes instalar y lanzar la base de datos MongoDB.
Instalación de MongoDB en Mac
Si usas una Mac con Homebrew, puedes ejecutar el siguiente comando:
$ brew install mongodb
Con MacPorts, puedes ejecutar el siguiente comando:
$ port install mongodb
Instalación de MongoDB en Windows y otras plataformas
Después de instalar MongoDB, puedes iniciarlo en una ventana de consola ejecutando el siguiente comando (que también inicia un proceso de servidor):
$ mongod
Deberías ver una salida similar a la siguiente:
all output going to: /usr/local/var/log/mongodb/mongo.log
4. Creación de una “Endidad” Usuario
MongoDB es una base de datos documental NoSQL. En esta práctica, almecenaremos objetos Usuario. El siguiente código muestra la clase Usuario (en src/main/java/com/javhoz/ad/mongodb/Usuario.java):
La clase Usuario tiene tres atributos: idUsuario, nome y apelidos. El idUsuario es principalmente para uso interno de MongoDB. También dispone un constructor único para crear una nueva instancia y el constructor por defecto.
Se omiten los típicos getters y setters para simplificar el código.
El idUsuario no se ajusta al nombre estándar para un ID de MongoDB, pero podría emplearse una anotación para etiquetarlo para Spring Data MongoDB.
Para ello, añade la anotación @Id a la propiedad idUsuario. También podrías usar la anotación @Field para especificar el nombre del campo en la base de datos.
Las otras dos propiedades, nome y apelidos, se dejan sin anotación. Se asume que se asignan a campos que comparten el mismo nombre que las propiedades mismas.
El conveniente sobrescribir el método toString() imprime los detalles sobre un cliente.
MongoDB almacena datos en colecciones (equivalente a una table en un SGBDR). Spring Data MongoDB mapea la clase Usuario a una colección llamada usuario. Si deseas cambiar el nombre de la colección, puedes usar la anotación @Document de Spring Data MongoDB en la clase.
5. Creación de consultas con Spring Data MongoDB
Spring Data MongoDB se centra en almacenar datos en MongoDB. Como hereda funcionalidades del proyecto Spring Data Commons, tiene la capacidad de derivar consultas. Esencialmente, no se necesita aprender el lenguaje de consulta de MongoDB. Puedes emplear muchos métodos y las consultas se escriben de manera automática.
Creación de un Repositorio
Para ver cómo funciona esto, crea una interfaz de repositorio que consulte documentos de Usuario, como se muestra en el siguiente código (en src/main/java/com/javhoz/ad/mongodb/RepositorioUsuario.java):
package com.javhoz.ad.mongodb;
import java.util.List;
import org.springframework.data.mongodb.repository.MongoRepository;
publicinterfaceRepositorioUsuarioextends MongoRepository<Usuario, String> {
public Usuario findByNome(String nome);
public List<Usuario>findByApelidos(String apelidos);
}
RepositorioUsuario extiende la interfaz MongoRepository e incorpora el tipo de valores e ID con los que trabaja: Usuario y String, respectivamente. Esta interfaz viene con muchas operaciones, incluidas las operaciones CRUD estándar (crear, leer, actualizar y eliminar).
Puedes definir otras consultas declarando sus firmas de método. En este caso, agrega findByNome, que esencialmente busca documentos de tipo Usuario y encuentra los documentos que coinciden con nome.
También he añadido findByApelidos, que encuentra una lista de personas por apellido.
En una aplicación Java típica, se implementaría una clase que implemente RepositorioUsuario y se crearía la consulta personalizada. Spring Data MongoDB no necesitas crear esta implementación, lo que lo hace muy útil. Spring Data MongoDB la crea dinámicamente cuando ejecutas la aplicación.
6. Crear una Clase de Aplicación
Spring Initializr crea una clase sencilla para la aplicación. El siguiente código muestra la clase que Initializr creó para este ejemplo (en src/main/java/com/javhoz/ad/mongodb/AccesoDatosMongodbApplication.java, pero depende del nombre que le hayas dado al proyecto en el inicializador de Spring):
@SpringBootApplication es una anotación útil que añade todo lo siguiente:
@Configuration: etiqueta la clase como fuente de definiciones de bean para el contexto de la aplicación.
@EnableAutoConfiguration: le dice a Spring Boot que comience a agregar beans en función de la configuración de la ruta de clase, otros beans y varias configuraciones de propiedades. Por ejemplo, si spring-webmvc está en la ruta de clase, esta anotación indica que la aplicación es una aplicación web y activa comportamientos clave, como configurar un DispatcherServlet.
@ComponentScan: le dice a Spring que busque otros componentes, configuraciones y servicios en el paquete com/javhoz, permitiéndole encontrar los controladores.
El método main() utiliza el método SpringApplication.run() de Spring Boot para lanzar una aplicación. Esta aplicación web es 100% Java puro y no hay que “pelearse” con la configuración de la estructura (web.xml, etc.).
Gestión de repositorios
Spring Boot gestiona automáticamente esos repositorios siempre que estén incluidos en el mismo paquete (o un subpaquete) que tu clase @SpringBootApplication. Para tener un mayor control sobre el proceso de registro, puedes usar la anotación @EnableMongoRepositories.
Por defecto, @EnableMongoRepositories escanea el paquete actual en busca de interfaces que extiendan una de las interfaces de repositorio de Spring Data.
Se puede usar basePackageClasses=MiRepository.class para decirle de manera segura a Spring Data MongoDB que escanee un paquete raíz diferente por tipo si la disposición de tu proyecto tiene múltiples proyectos y no encuentra tus repositorios.
Spring Data MongoDB utiliza MongoTemplate para ejecutar las consultas detrás de tus métodos find*. Puedes usar la plantilla tú mismo para consultas más complejas, de momento lo dejamos así (podéis consultar la Guía de Referencia de Spring Data MongoDB para más detalles):
Modificaremos la clase de la aplicación que creó Initializr para ti que cree documentos, para configurar algunos datos y usarlos para generar salida.
El siguiente código muestra la clase AccesoDatosMongodbApplication completa (en src/main/java/com/javhoz/ad/mongodb/AccesoDatosMongodbApplication.java):
package com.javhoz.ad.mongodb;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublicclassAccesoDatosMongodbApplicationimplements CommandLineRunner {
@Autowiredprivate RepositorioUsuario repository;
publicstaticvoidmain(String[] args) {
SpringApplication.run(AccesoDatosMongodbApplication.class, args);
}
@Overridepublicvoidrun(String... args) throws Exception {
repository.deleteAll();
// guarda varios usuarios, con nombres y apellidos de mujeres científicas repository.save(new Usuario("Ada", "Lovelace")); // Ada Lovelace fue una matemática y escritora británica, conocida por su trabajo sobre la máquina analítica de Charles Babbage repository.save(new Usuario("Marie", "Curie")); // Marie Curie fue una científica polaca, nacionalizada francesa, pionera en el campo de la radiactividad repository.save(new Usuario("Grace", "Hopper")); // Grace Hopper fue una científica de la computación y almirante de la Marina de los Estados Unidos. Fue una de las primeras programadoras de la historia. repository.save(new Usuario("Barbara", "Liskov")); // Barbara Liskov es una científica de la computación que desarrolló el lenguaje de programación CLU y fue la primera mujer en recibir un doctorado en informática en los Estados Unidos// Muestra todos los usuarios System.out.println("Usuarias encontradas con findAll():");
System.out.println("-------------------------------");
for (Usuario usuario : repository.findAll()) {
System.out.println(usuario);
}
System.out.println();
// Busca un usuario por su nombre System.out.println("Usuario encontrado con findByNome('Marie'):");
System.out.println("--------------------------------");
System.out.println(repository.findByNome("Marie"));
System.out.println("Usuarios encontrados con findByApelidos('Liskov'):");
System.out.println("--------------------------------");
for (Usuario usuario : repository.findByApelidos("Liskov")) {
System.out.println(usuario);
}
}
}
AccesoDatosMongodbApplication incluye un método main() que realiza la inyección de dependencias de una instancia de RepositorioUsuario.
Spring Data MongoDB crea dinámicamente un proxy y lo inyecta ahí.
Utilizamos RepositorioUsuario a través de algunas pruebas:
a) Elimina todas las entidades con deleteAll(), demostrando el método deleteAll() y configurando algunos datos para usar.
b) Llama a findAll() para obtener todos los objetos Usuario de la base de datos.
c) Invoca a findByNome() para obtener un único Usuario por su nombre.
d) findByApelidos() para encontrar todos los clientes cuyo apellido es Liskov.
Por defecto, Spring Boot intenta conectarse a una instancia de MongoDB alojada localmente. Lee la documentación de referencia para obtener detalles sobre cómo apuntar tu aplicación a una instancia de MongoDB alojada en otro lugar.
Para hacerlo, puedes configurar una conexión a MongoDB en el archivo application.properties:
Puedes ejecutar la aplicación desde la línea de comandos con Gradle o Maven.
También puedes construir un solo archivo JAR ejecutable que contenga todas las dependencias, clases y recursos necesarios y ejecutarlo.Construir un JAR ejecutable facilita el envío, versionado e implementación del servicio como una aplicación a lo largo del ciclo de desarrollo, en diferentes entornos, y así sucesivamente.
Si usas Gradle, puedes ejecutar la aplicación con ./gradlew bootRun. Alternativamente, puedes construir el archivo JAR con ./gradlew build y luego ejecutar el archivo JAR, así:
Si usas Maven, puedes ejecutar la aplicación con ./mvnw spring-boot:run. Alternativamente, puedes construir el archivo JAR con ./mvnw clean package y luego ejecutar el archivo JAR, así:
Los pasos descritos aquí crean un JAR ejecutable. También puedes construir un archivo WAR clásico.
Dado que AccesoDatosMongodbApplication implementa CommandLineRunner, el método run se invoca automáticamente cuando Spring Boot se inicia. Deberías ver algo como lo siguiente (con otra salida, como consultas, también):
Todos los contenidos sobre GIT son basados en el libro oficial Pro GIt v2
Subsecciones de Git
Introducción git
Nota importante
Todos los contenidos sobre GIT son basados en el libro oficial Pro GIt v2
Subsecciones de Introducción git
Introducción a git
Control de Versiones
Un control de versiones es un sistema que registra los cambios realizados en un archivo o conjunto de archivos a lo largo del tiempo, de modo que puedas recuperar versiones específicas más adelante.
Usar un VCS (Sistema de Control de Versiones) también significa generalmente que si arruinas o pierdes archivos, será posible recuperarlos fácilmente. Adicionalmente, obtendrás todos estos beneficios a un costo muy bajo.
Sistemas de control de versiones locales
Un método de control de versiones, usado por muchas personas, es copiar los archivos a otro directorio. Este método es muy común porque es muy sencillo, pero también es tremendamente propenso a errores. Es fácil olvidar en qué directorio te encuentras y guardar accidentalmente en el archivo equivocado o sobrescribir archivos que no querías.
Para afrontar este problema los programadores desarrollaron hace tiempo VCS locales que contenían una simple base de datos, en la que se llevaba el registro de todos los cambios realizados a los archivos.
Sistemas de control de versiones centralizados
El siguiente gran problema con el que se encuentran las personas es que necesitan colaborar con desarrolladores en otros sistemas. Los sistemas de Control de Versiones Centralizados (CVCS por sus siglas en inglés) fueron desarrollados para solucionar este problema.
Esta configuración ofrece muchas ventajas, especialmente frente a VCS locales. Por ejemplo, todas las personas saben hasta cierto punto en qué están trabajando los otros colaboradores del proyecto. Los administradores tienen control detallado sobre qué puede hacer cada usuario, y es mucho más fácil administrar un CVCS que tener que lidiar con bases de datos locales en cada cliente.
Sin embargo, esta configuración también tiene serias desventajas. La más obvia es el punto único de fallo que representa el servidor centralizado. Si ese servidor se cae durante una hora, entonces durante esa hora nadie podrá colaborar o guardar cambios en archivos en los que hayan estado trabajando. Si el disco duro en el que se encuentra la base de datos central se corrompe, y no se han realizado copias de seguridad adecuadamente, se perderá toda la información del proyecto, con excepción de las copias instantáneas que las personas tengan en sus máquinas locales.
Sistemas de control de versiones Distribuidos
Los sistemas de Control de Versiones Distribuidos (DVCS por sus siglas en inglés) ofrecen soluciones para los problemas que han sido mencionados. En un DVCS (como Git, Mercurial, Bazaar o Darcs), los clientes no solo descargan la última copia instantánea de los archivos, sino que se replica completamente el repositorio. De esta manera, si un servidor deja de funcionar y estos sistemas estaban colaborando a través de él, cualquiera de los repositorios disponibles en los clientes puede ser copiado al servidor con el fin de restaurarlo. Cada clon es realmente una copia completa de todos los datos.
Además, muchos de estos sistemas se encargan de manejar numerosos repositorios remotos con los cuales pueden trabajar, de tal forma que puedes colaborar simultáneamente con diferentes grupos de personas en distintas maneras dentro del mismo proyecto. Esto permite establecer varios flujos de trabajo que no son posibles en sistemas centralizados, como pueden ser los modelos jerárquicos.
Características de git
Git maneja sus datos como un conjunto de copias instantáneas de un sistema de archivos miniatura. Cada vez que confirmas un cambio, o guardas el estado de tu proyecto en Git, él básicamente toma una foto del aspecto de todos tus archivos en ese momento y guarda una referencia a esa copia instantánea. Para ser eficiente, si los archivos no se han modificado Git no almacena el archivo de nuevo, sino un enlace al archivo anterior idéntico que ya tiene almacenado. Git maneja sus datos como una secuencia de copias instantáneas.
Copias instantánes, no diferenciadas
Git maneja sus datos como un conjunto de copias instantáneas de un sistema de archivos miniatura. Cada vez que confirmas un cambio, o guardas el estado de tu proyecto en Git, él básicamente toma una foto del aspecto de todos tus archivos en ese momento y guarda una referencia a esa copia instantánea. Para ser eficiente, si los archivos no se han modificado Git no almacena el archivo de nuevo, sino un enlace al archivo anterior idéntico que ya tiene almacenado. Git maneja sus datos como una secuencia de copias instantáneas.
Casi todas las operaciones son locales.
La mayoría de las operaciones en Git sólo necesitan archivos y recursos locales para funcionar. Por lo general no se necesita información de ningún otro computador de tu red.
Por ejemplo, para navegar por la historia del proyecto, Git no necesita conectarse al servidor para obtener la historia y mostrártela - simplemente la lee directamente de tu base de datos local. Esto significa que ves la historia del proyecto casi instantáneamente.
Esto también significa que hay muy poco que no puedes hacer si estás desconectado. Si te subes a un avión o a un tren y quieres trabajar un poco, puedes confirmar tus cambios felizmente hasta que consigas una conexión de red para subirlos.
Git tiene integridad
Todo en Git es verificado mediante una suma de comprobación (checksum en inglés) antes de ser almacenado, y es identificado a partir de ese momento mediante dicha suma. Esto significa que es imposible cambiar los contenidos de cualquier archivo o directorio sin que Git lo sepa.
Información
No puedes perder información durante su transmisión o sufrir corrupción de archivos sin que Git sea capaz de detectarlo.
El mecanismo que usa Git para generar esta suma de comprobación se conoce como hash SHA-1.
24b9da6552252987aa493b52f8696cd6d3b00373
Git sólo añade información
Cuando realizas acciones en Git, casi todas ellas sólo añaden información a la base de datos de Git. Es muy difícil conseguir que el sistema haga algo que no se pueda enmendar, o que de algún modo borre información.
Git tiene tres estados principales en los que se pueden encontrar tus archivos:
Confirmado (committed): significa que los datos están almacenados de manera segura en tu base de datos local.
Modificado (modified): significa que has modificado el archivo pero todavía no lo has confirmado a tu base de datos.
Preparado (staged): significa que has marcado un archivo modificado en su versión actual para que vaya en tu próxima confirmación.
Esto nos lleva a las tres secciones principales de un proyecto de Git:
El directorio de Git (Git directory) es donde se almacenan los metadatos y la base de datos de objetos para tu proyecto. Es la parte más importante de Git, y es lo que se copia cuando clonas un repositorio desde otra computadora.
El directorio de trabajo (Working directory) es una copia de una versión del proyecto. Estos archivos se sacan de la base de datos comprimida en el directorio de Git, y se colocan en disco para que los puedas usar o modificar.
El área de preparación (Staging area). Es un archivo, generalmente contenido en tu directorio de Git, que almacena información acerca de lo que va a ir en tu próxima confirmación.
El flujo de trabajo básico en Git es algo así:
Modificas una serie de archivos en tu directorio de trabajo.
Preparas los archivos, añadiéndolos a tu área de preparación.
Confirmas los cambios, lo que toma los archivos tal y como están en el área de preparación y almacena esa copia instantánea de manera permanente en tu directorio de Git.
Antes de empezar a utilizar Git, tienes que instalarlo en tu computadora.
Instalación en Linux
Si quieres instalar Git en Linux a través de un instalador binario, en general puedes hacerlo mediante la herramienta básica de administración de paquetes que trae tu distribución.
$ apt-get install git
Instalación en Mac
Instale homebrew si aún no lo tiene, luego ejecutar:
$ brew install git
Instalación en Windows
Solo tienes que visitar la página oficial, seleccionar la opción correspondiente y la descarga empezará automáticamente.
Todos los contenidos sobre GIT son basados en el libro oficial Pro GIt v2
Subsecciones de Fundamentos git
Configuracion de git
Lo primero que deberás hacer cuando instales Git es establecer tu nombre de usuario y dirección de correo electrónico. Esto es importante porque los “commits” de Git usan esta información, y es introducida de manera inmutable en los commits que envías:
Sólo necesitas hacer esto una vez si especificas la opción –global, ya que Git siempre usará esta información para todo lo que hagas en ese sistema. Si quieres sobrescribir esta información con otro nombre o dirección de correo para proyectos específicos, puedes ejecutar el comando sin la opción –global cuando estés en ese proyecto.
Comprobando configuración
Si quieres comprobar tu configuración, puedes usar el comando git config –list para mostrar todas las propiedades que Git ha configurado:
También puedes comprobar el valor que Git utilizará para una clave específica ejecutando git config
$ git help <verbo>
$ git <verbo> --help
Inicializando repositorio
Para inicializar el repositorio en local realizamos los siguientes pasos:
Comprobamos la ruta en la que nos encontramos (comando pwd)
$ pwd
/Users/sabelasobrinosanmartin
Nos movemos hasta el escritorio de mi usuario (comando cd)
$ cd Desktop
Creamos una carpeta que va a almacenar el repositorio (comando mkdir).
$ mkdir repositorio
Nos movemos dentro de la carpeta que acabamos de crear (comando cd).
$ cd repositorio
Inicializamos el repositorio dentro de esa carpeta. (comando git init).
$ git init .
ayuda: Usando 'master' como el nombre de la rama inicial. Este nombre de rama predeterminado
ayuda: está sujeto a cambios. Para configurar el nombre de la rama inicial para usar en todos
ayuda: de sus nuevos repositorios, reprimiendo esta advertencia, llama a:
ayuda:
ayuda: git config --global init.defaultBranch <nombre>
ayuda:
ayuda: Los nombres comúnmente elegidos en lugar de 'master' son 'main', 'trunk' y
ayuda: 'development'. Se puede cambiar el nombre de la rama recién creada mediante este comando:
ayuda:
ayuda: git branch -m <nombre>
Inicializado repositorio Git vacío en /Users/sabelasobrinosanmartin/Desktop/repositorio/.git/
Nos fijamos en dos cosas, la primera es que al ejecutar el comando nos indica que se ha inicializado correctamente el repositorio en la ruta elegida.
Inicializado repositorio Git vacío en /Users/sabelasobrinosanmartin/Desktop/repositorio/.git/
La segunda es que a partir de ahora siempre que naveguemos dentro de nuestra carpeta tendremos al final de la ruta la versión en la que estamos trabajando (master) en este caso.
Si ejecutamos el comando ls –la (para que nos muestre los ficheros ocultos) veremos los siguientes:
Comprobamos que nos crea una carpeta .git que será la base del repositorio. En el caso de que queramos eliminar el repositorio lo único que tendremos que hacer es eliminar esa carpeta (comando rm).
$ rm -rf .git/
Una vez eliminada esa carpeta podemos ver que ya no nos muestra la versión en la que estamos trabajando (master).
Para poder trabajar con nuestro repositorio vamos a inicializarlo de nuevo y a crear dos nuevos ficheros con los comandos nano o vim:
$ git init
$ nano canciones.txt
19 días y 500 noches, Joaquín Sabina
$ nano libros.txt
1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón
Cambios en el repositorio
Ya tenemos un repositorio Git y un checkout o copia de trabajo de los archivos de dicho proyecto. El siguiente paso es realizar algunos cambios y confirmar instantáneas de esos cambios en el repositorio cada vez que el proyecto alcance un estado que quieras conservar.
Git status
El comando git status es la herramienta principal para determinar qué archivos están en qué estado.
$ git status
En la rama master
No hay commits todavía
Archivos sin seguimiento:
(usa "```bash add``` <archivo>..." para incluirlo a lo que se será confirmado) canciones.txt
libros.txt
no hay nada agregado al commit pero hay archivos sin seguimiento presentes (usa "```bash add```" para hacerles seguimiento)
Puedes ver que los archivos canciones.txt y libros.txt están sin seguimiento porque aparece debajo del encabezado “Untracked files” (“Archivos no rastreados” en inglés) en la salida.
Este estado, “Sin seguimiento” significa que Git ve archivos que no tenías antes. Git no los incluirá en tu próximo commit (próxima confirmación) a menos que se lo indiques explícitamente.
Se comporta así para evitar incluir accidentalmente archivos binarios o cualquier otro archivo que no quieras incluir. Si queremos incluir los dos ficheros en el próximo commit debemos restrearlos.
Git add
Para comenzar a realizar un seguimiento de los archivos debes usar el comando bash add. Podemos ejecutar dos comandos uno por cada archivo.
$ git add canciones.txt
$ git add libros.txt
O realizar un único bash add . , que incluye todo lo que hay en la carpeta:
$ git add .
Si la salida produce algún mensaje de salida (los warnings) se debe a que estamos trabajando en una consola Linux dentro del sistema operativo de Windows.
Si ahora ejecutamos un git status para ver el estado de los archivos tendremos algo como esto:
$ git status
En la rama master
No hay commits todavía
Cambios a ser confirmados:
(usa "git rm --cached <archivo>..." para sacar del área de stage) nuevos archivos: canciones.txt
nuevos archivos: libros.txt
Puedes ver que está siendo rastreado porque aparece luego del encabezado “Cambios a ser confirmados” (“Changes to be committed” en inglés).
Si confirmas los cambios en este punto, se guardará en el historial la versión del archivo correspondiente al instante en que ejecutaste bash add, pudiendo volver al punto de partida del archivo siempre que quieras.
Git commit
Ahora que tu área de trabajo está como quieres, puedes confirmar tus cambios. Recuerda que cualquier cosa que no esté preparada -cualquier archivo que hayas creado o modificado y que no hayas agregado con bash add desde su edición- no será confirmado.
A través del comando –m le indicamos el mensaje que veremos después.
Puedes ver que la confirmación te devuelve una salida descriptiva: indica cuál rama has confirmado (master), que checksum SHA-1 tiene el commit (2823aec), cuántos archivos han cambiado y estadísticas sobre las líneas añadidas y eliminadas en el commit.
Recuerda que la confirmación guarda una instantánea de tu área de trabajo. Todo lo que no hayas preparado sigue allí modificado; puedes hacer una nueva confirmación para añadirlo a tu historial. Cada vez que realizas un commit, guardas una instantánea de tu proyecto la cual puedes usar para comparar o volver a ella luego.
Archivos Ignorados
A veces, tendrás algún tipo de archivo que no quieres que Git añada automáticamente o que ni siquiera quieras que aparezca como no rastreado. Este suele ser el caso de archivos generados automáticamente como trazas o archivos creados por tu sistema de compilación. En estos casos, puedes crear un archivo llamado .gitignore que liste patrones o archivos a considerar.
Para ver esto, vamos a crearnos un nuevo archivo que queremos que sea ignorado:
$ nano system.log
Y vamos a modificar canciones.txt añadiendo una nueva línea:
$ nano libros.txt
1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón
Si ahora hacemos un git status comprobamos que tenemos dos archivos, uno modificado (libros.txt) y el otro sin realizar ningún seguimiento (system.log)
$ git status
En la rama master
Cambios no rastreados para el commit:
(usa "git add <archivo>..." para actualizar lo que será confirmado)(usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo) modificados: libros.txt
Archivos sin seguimiento:
(usa "git add <archivo>..." para incluirlo a lo que se será confirmado) system.log
sin cambios agregados al commit (usa "git add" y/o "git commit -a")
Lo que vamos a hacer es crear un fichero .gitignore y añadirle el fichero system.log para que no se realice ningún seguimiento sobre él, ya que no nos interesa.
$ nano .gitignore
system.log
Ahora mismo tenemos los siguientes ficheros en nuestra carpeta:
Si ahora hacemos un git status vemos que tenemos el fichero libros.txt modificado y el .gitignore para realizar el seguimiento pero ya no tenemos system.log porque lo hemos añadido al fichero .gitignore:
$ git status
En la rama master
Cambios no rastreados para el commit:
(usa "git add <archivo>..." para actualizar lo que será confirmado)(usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo) modificados: libros.txt
Archivos sin seguimiento:
(usa "git add <archivo>..." para incluirlo a lo que se será confirmado) .gitignore
sin cambios agregados al commit (usa “git add” y/o “git commit -a”)
Vamos a hacer un bash add sobre esos dos ficheros para marcarlos:
En este momento tenemos los dos ficheros rastreados y listos para su confirmación:
Ya vimos el funcionamiento del archivo .gitignore, en el que podemos añadir los nombres de los ficheros que no queremos que sean rastreados, pero también podemos añadir patrones de rastreo, lo que nos permitirá crear reglas y no tener que añadir todos los nombres de los ficheros.
Las reglas sobre los patrones que puedes incluir en el archivo .gitignore son las siguientes:
Ignorar las líneas en blanco y aquellas que comiencen con #.
Aceptar patrones glob estándar.
Los patrones pueden terminar en barra (/) para especificar un directorio.
Los patrones pueden negarse si se añade al principio el signo de exclamación (!).
Los patrones glob son una especie de expresión regular simplificada usada por los terminales.
Un asterisco (*) corresponde a cero o más caracteres.
[abc] corresponde a cualquier caracter dentro de los corchetes (en este caso a, b o c).
El signo de interrogación (?) corresponde a un caracter cualquiera-
Los corchetes sobre caracteres separados por un guión ([0-9]) corresponde a cualquier caracter entre ellos (en este caso del 0 al 9).
También puedes usar dos asteriscos para indicar directorios anidados; a/**/z coincide con a/z, a/b/z, a/b/c/z, etc.
Con el comando checkout podemos volver a una versión anterior de nuestro documento. En este caso vamos a revertir los cambios en libros.txt eliminando la línea nueva del último commit. Actualmente el archivo está como sigue:
1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón
Si queremos volver a la versión anterior en la cual sólo estaba la línea “1984, George Orwell” del primer commit, lo primero que tenemos que hacer es un git log. Con este comando podemos ver el hash (ese número tan largo) de cada commit:
Para revertir los cambios tenemos que copiar los 10 primeros números del commit donde está la versión de nuestro documento que nos interesa (en este caso mensaje inicial) y pegarlos juntos al comando bashcheckout:
git checkout 2823aec8fac
Nota: cambiando a '2823aec8fac'.
Te encuentras en estado 'detached HEAD'. Puedes revisar por aquí, hacer
cambios experimentales y hacer commits, y puedes descartar cualquier
commit que hayas hecho en este estado sin impactar a tu rama realizando
otro checkout.
HEAD está ahora en 2823aec mensaje inicial
Si después de realizar el checkout comprobamos el contenido del fichero (canciones.txt) vemos que ha sido modificado y que además nos cambia la versión en la que estamos trabajando ya no es la master sino una antigua.
$ more libros.txt
1984, George Orwell
Como hemos revertido todos los cambios, si hacemos un ls –la ya no veremos el archivo .gitignore
Para volver a la rama master simplemente hacemos un checkout a la rama master
$ git checkout master
Git Log
Después de haber hecho varias confirmaciones, probablemente quieras mirar atrás para ver qué modificaciones se han llevado a cabo. La herramienta más básica y potente para hacer esto es el comando git log.
Si hacemos un git log en este momento tendremos las siguientes líneas de confirmación:
Vamos a cambiar un archivo que esté rastreado. Nos vale cualquier archivo, vamos a modificar el archivo libros.txt:
$ nano libros.txt
1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón
la cuidad y los perros, Mario Vargas Llosa
Si ahora ejecutamos un git status veremos algo parecido a esto:
$ git status
En la rama master
Cambios no rastreados para el commit:
(usa "git add <archivo>..." para actualizar lo que será confirmado)(usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo) modificados: libros.txt
sin cambios agregados al commit (usa "git add" y/o "git commit -a")
El archivo “libros.txt” aparece en una sección llamada “Cambios no rastreados para el commit” - lo que significa que existe un archivo rastreado que ha sido modificado en el directorio de trabajo pero que aún no está preparado. Para prepararlo, ejecutamos el comando git add.
$ git add .
Al ejecutar un git status comprobamos que todo está correcto:
$ git status
En la rama master
Cambios a ser confirmados:
(usa "git restore --staged <archivo>..." para sacar del área de stage) modificados: libros.txt
El archivo está preparados y formará parte de tu próxima confirmación. En este momento, supongamos que recuerdas que debes hacer un pequeño cambio en canciones.txt antes de confirmarlo. Abres de nuevo el archivo, lo cambias y ahora estás listo para confirmar.
1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón
la cuidad y los perros, Mario Vargas Llosa
El libro negro de las horas, Eva García Sáenz de Urturi
Sin embargo, ejecutemos git status una vez más:
$ git status
En la rama master
Cambios a ser confirmados:
(usa "git restore --staged <archivo>..." para sacar del área de stage) modificados: libros.txt
Cambios no rastreados para el commit:
(usa "git add <archivo>..." para actualizar lo que será confirmado)(usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo) modificados: libros.txt
Ahora libros.txt aparece como preparado y no preparado. Resulta que Git prepara un archivo de acuerdo al estado que tenía cuando ejecutas el comando git add.
Si confirmas ahora, se confirmará la versión de libros.txt que tenías la última vez que ejecutaste git add y no la versión que ves ahora en tu directorio de trabajo al ejecutar git status. Si modificas un archivo luego de ejecutar git add, deberás ejecutar git add de nuevo para preparar la última versión del archivo:
$ git add .
$ git status
En la rama master
Cambios a ser confirmados:
(usa "git restore --staged <archivo>..." para sacar del área de stage) modificados: libros.txt
$ git commit -m "archivos modificados"[master fcff217] archivos modificados
1 file changed, 3 insertions(+)
Archivos modificados
Estado abreviado
Si bien es cierto que la salida de git status es bastante explícita, también es verdad que es muy extensa. Podemos usar el siguiente comando:
$ git status --short
Que nos ofrece una salida abreviada bajo las siguientes siglas:
Archivo modificado (M)
Archivo no rastreado (??)
Archivo preparado (A).
Ver cambios no preparados
Vamos a modificar el archivo canciones.txt:
$ nano canciones.txt
19 días y 500 noches, Joaquín Sabina
Por la raja de tu falda, Estopa
Para ver qué has cambiado pero aún no has preparado, escribe git diff sin más parámetros:
$ git diff
diff --git a/canciones.txt b/canciones.txt
index 7898192..32c4828 100644--- a/canciones.txt
+++ b/canciones.txt
@@ -1 +1,2 @@
-a
+Por la raja de tu falda, Estopa
Nos dice que hemos modificado el archivo canciones.txt añadiendo la línea “Por la raja de tu falda, Estopa”.
Ahora vamos a preparar y confirmar todos los cambios:
$ nano libros.txt
1984, George Orwell
la cuidad y los perros, Mario Vargas Llosa
El libro negro de las horas, Eva García Sáenz de Urturi
Marina, Carlos Ruíz Zafón
Si ejecutamos un git diff nos indica que una línea ha sido borrada (rojo) y otra ha sido añadida (verde).
$ git diff
diff --git a/libros.txt b/libros.txt
index e808bd3..1810eb8 100644--- a/libros.txt
+++ b/libros.txt
@@ -1,4 +1,4 @@
1984, George Orwell
-la sombra del viento, Carlos Ruíz Zafón
la cuidad y los perros, Mario Vargas Llosa
El libro negro de las horas, Eva García Sáenz de Urturi
+Marina, Carlos Ruíz Zafón
Si quieres ver lo que has preparado y será incluido en la próxima confirmación, puedes usar git diff --staged. Este comando compara tus cambios preparados con la última instantánea confirmada. Si ejecutamos ahora git diff –staged no veremos nada (ya que no hay nada preparado).
$ git diff --staged
Si ahora ejecutamos un git diff no veremos nada, puesto que los cambios ya están preparados.
Es importante resaltar que al llamar a git diff sin parámetros no verás los cambios desde tu última confirmación - solo verás los cambios que aún no están preparados. Esto puede ser confuso porque si preparas todos tus cambios, git diff no te devolverá ninguna salida.
Opciones con archivos
Eliminar archivos
Si simplemente eliminas el archivo de tu directorio de trabajo, aparecerá en la sección “Changes not staged for commit” (esto es, sin preparar) en la salida de git status.
Vamos a eliminar el fichero canciones.txt:
$ rm canciones.txt
Si hacemos un git status, comprobamos que el archivo ha sido marcado para eliminar:
$ git status
En la rama master
Cambios no rastreados para el commit:
(usa "git add/rm <archivo>..." para actualizar a lo que se le va a hacer commit)(usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo) borrados: canciones.txt
sin cambios agregados al commit (usa "git add" y/o "git commit -a")
Ahora, si ejecutas git rm, entonces se prepara la eliminación del archivo:
$ git rm canciones.txt
rm 'canciones.txt'
Con la próxima confirmación, el archivo habrá desaparecido y no volverá a ser rastreado.
$ git status
En la rama master
Cambios a ser confirmados:
(usa "git restore --staged <archivo>..." para sacar del área de stage) borrados: canciones.txt
Dejar de rastrear archivos
Si ahora modificamos el archivo “nuevo archivo.txt”
Nos aparece como archivo sin seguimiento:
Cambiar el nombre archivos
Para cambiar el nombre de un archivo tenemos el siguiente comando:
$ git mv canciones.txt peliculas.txt
Historial de confirmaciones
Opciones del historial de confirmaciones
Después de haber hecho varias confirmaciones, o si has clonado un repositorio que ya tenía un histórico de confirmaciones, probablemente quieras mirar atrás para ver qué modificaciones se han llevado a cabo. La herramienta más básica y potente para hacer esto es el comando git log.
$ git log
Se ven las confirmaciones en orden cronológico inverso.
Una de las opciones más útiles es -p, que muestra las diferencias introducidas en cada confirmación. También puedes usar la opción -2, que hace que se muestren únicamente las dos últimas entradas del historial:
$ git log -p -2
Limitar la salida
las opciones temporales como –since (desde) y –until (hasta) resultan muy útiles. Por ejemplo, este comando lista todas las confirmaciones hechas durante las dos últimas semanas:
$ git log --since=2.weeks
Deshacer
En cualquier momento puede que quieras deshacer algo. Uno de las acciones más comunes a deshacer es cuando confirmas un cambio antes de tiempo y olvidas agregar algún archivo, o te equivocas en el mensaje de confirmación. Si quieres rehacer la confirmación, puedes reconfirmar con la opción –amend.
$ git commit --ammend -m "nuevo commit que modifica el anterior"
Remotos
Los repositorios remotos son versiones de tu proyecto que están hospedadas en Internet o en cualquier otra red. Puedes tener varios de ellos, y en cada uno tendrás generalmente permisos de solo lectura o de lectura y escritura.
Colaborar con otras personas implica gestionar estos repositorios remotos enviando y trayendo datos de ellos cada vez que necesites compartir tu trabajo.
Obtener un repositorio remoto
Para obtener un repositorio remoto debemos usar el comando git clone junto con la url del repositorio:
Para ver los repositorios remotos que tienes configurados, debes ejecutar el comando git remote dentro de la carpeta del repositorio.
Si has clonado tu repositorio, deberías ver al menos origin (origen, en inglés) - este es el nombre que por defecto Git le da al servidor del que has clonado.
$ git remote
origin
También puedes pasar la opción -v, la cual muestra las URLs que Git ha asociado al nombre y que serán usadas al leer y escribir en ese remoto:
A partir de ahora puedes usar el nombre daw1 en la línea de comandos en lugar de la URL entera.
Traer y Combinar Remotos
Para obtener datos de tus proyectos remotos puedes ejecutar:
$ git fetch [remote-name]
El comando irá al proyecto remoto y se traerá todos los datos que aun no tienes de dicho remoto. Luego de hacer esto, tendrás referencias a todas las ramas del remoto, las cuales puedes combinar e inspeccionar cuando quieras.
$ git fetch origin
Este comando se trae todo el trabajo nuevo que ha sido enviado a ese servidor desde que lo clonaste (o desde la última vez que trajiste datos). Es importante destacar que el comando git fetch solo trae datos a tu repositorio local, ni lo combina automáticamente con tu trabajo ni modifica el trabajo que llevas hecho. La combinación con tu trabajo debes hacerla manualmente cuando estés listo.
Enviar a tus remotos
Cuando tienes un proyecto que quieres compartir, debes enviarlo a un servidor. El comando para hacerlo es simple: git push [nombre-remoto] [nombre-rama].
Si quieres enviar tu rama master a tu servidor origin, entonces puedes ejecutar el siguiente comando y se enviarán todos los commits que hayas hecho al servidor:
$ git push origin master
remotos
Este comando solo funciona si clonaste de un servidor sobre el que tienes permisos de escritura y si nadie más ha enviado datos por el medio. Si alguien más clona el mismo repositorio que tú y envía información antes que tú, tu envío será rechazado. Tendrás que traerte su trabajo y combinarlo con el tuyo antes de que puedas enviar datos al servidor.
Eliminar y renombrar remotos
Si quieres cambiar el nombre de la referencia de un remoto puedes ejecutar git remote rename. Por ejemplo, si quieres cambiar el nombre de daw1 a daw2:
$ git remote rename daw1 daw2
Si por alguna razón quieres eliminar un remoto puedes usar git remote rm:
$ git remote rm daw1
Gitlab
En el vídeo se muestra un repositorio remoto con Bitbucket. Este repositorio es similar al que tenemos nosotros/as en el Gitlab. Simplemente cambia la interfaz.
Etiquetado
Git tiene la posibilidad de etiquetar puntos específicos del historial como importantes. Esta funcionalidad se usa típicamente para marcar versiones de lanzamiento (v1.0, por ejemplo).
Listar Tus Etiquetas
Listar las etiquetas disponibles en Git es sencillo. Simplemente escribe git tag:
$ git tag
v0.1
v1.3
Información
Este comando lista las etiquetas en orden alfabético; el orden en el que aparecen no tiene mayor importancia.
Crear etiquetas
Para crear una etiqueta podemos usar el comando git tag.
$ git tag -a v1.4 -m 'my version 1.4'$ git tag
v0.1
v1.3
v1.4
La opción -m especifica el mensaje de la etiqueta, el cual es guardado junto con ella. Si no especificas el mensaje de una etiqueta anotada, Git abrirá el editor de texto para que lo escribas.
Puedes ver la información de la etiqueta junto con el commit que está etiquetado al usar el comando git show:
$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date: Sat May 3 20:19:12 2014 -0700
my version 1.4
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the version number
Compartir etiquetas
Por defecto, el comando git push no transfiere las etiquetas a los servidores remotos. Debes enviar las etiquetas de forma explícita al servidor luego de que las hayas creado. Este proceso es similar al de compartir ramas remotas - puedes ejecutar git push origin [etiqueta].
Todos los contenidos sobre GIT son basados en el libro oficial Pro GIt v2
Subsecciones de Ramas Git
Ramificación
Git almacena los datos como una serie de instantáneas (copias puntuales de los archivos completos, tal y como se encuentran en ese momento).
En cada confirmación de cambios (commit), Git almacena una instantánea de tu trabajo preparado. Dicha instantánea contiene además unos metadatos con el autor y el mensaje explicativo, y uno o varios apuntadores a las confirmaciones (commit).
Vamos a realizar un ejemplo y para ello creamos un nuevo repositorio:
$ mkdir ramas
$ cd ramas/
$ git init .
Initialized empty Git repository in /home/sanclemente.local/sabela/Escritorio/ramas/.git/
Cuando creas una confirmación con el comando git commit, Git realiza sumas de control de cada subdirectorio, y las guarda como objetos árbol en el repositorio Git. Después, Git crea un objeto de confirmación con los metadatos pertinentes y un apuntador al objeto árbol raíz del proyecto.
Si haces más cambios y vuelves a confirmar, la siguiente confirmación guardará un apuntador a su confirmación precedente.
Una rama Git es simplemente un apuntador móvil apuntando a una de esas confirmaciones. La rama por defecto de Git es la rama master o la rama main. Con la primera confirmación de cambios que realicemos, se creará esta rama principal master apuntando a dicha confirmación. En cada confirmación de cambios que realicemos, la rama irá avanzando automáticamente.
Información
La rama “master” en Git, no es una rama especial. Es como cualquier otra rama. La única razón por la cual aparece en casi todos los repositorios es porque es la que crea por defecto el comando git init y la gente no se molesta en cambiarle el nombre.
Crear nueva rama
Cuando creamos una nueva rama simplemente se crea un nuevo apuntador para que lo puedas mover libremente. Por ejemplo, supongamos que quieres crear una rama nueva denominada “testing”. Para ello, usarás el comando git branch:
$ git branch testing
Esto creará un nuevo apuntador apuntando a la misma confirmación donde estés actualmente.
Git sabe en qué rama estás en este momento mediante un apuntador especial denominado HEAD que apunta a la rama local en la que tú estés en ese momento, en este caso la rama master; pues el comando git branch solamente crea una nueva rama, pero no salta a dicha rama.
Cambiar de rama
Para saltar de una rama a otra, tienes que utilizar el comando git checkout. Hagamos una prueba, saltando a la rama testing recién creada:
$ git checkout testing
Esto mueve el apuntador HEAD a la rama testing.
¿Cuál es el significado de todo esto? lo veremos tras realizar otra confirmación de cambios:
$ nano fichero1.txt
$ git commit -a -m 'cambio en fichero 1'
Observamos algo interesante: la rama testing avanza, mientras que la rama master permanece en la confirmación donde estaba cuando lanzaste el comando git checkout para saltar. Volvamos ahora a la rama master:
$ git checkout master
Este comando realiza dos acciones: Mueve el apuntador HEAD de nuevo a la rama master, y revierte los archivos de tu directorio de trabajo; dejándolos tal y como estaban en la última instantánea confirmada en dicha rama master. Esto supone que los cambios que hagas desde este momento en adelante, divergirán de la antigua versión del proyecto. Básicamente, lo que se está haciendo es rebobinar el trabajo que habías hecho temporalmente en la rama testing; de tal forma que puedas avanzar en otra dirección diferente.
Ramas
Es importante destacar que cuando saltas a una rama en Git, los archivos de tu directorio de trabajo cambian. Si saltas a una rama antigua, tu directorio de trabajo retrocederá para verse como lo hacía la última vez que confirmaste un cambio en dicha rama. Si Git no puede hacer el cambio limpiamente, no te dejará saltar.
Haz algunos cambios más y confírmalos:
$ nano fichero2.txt
$ git commit -a -m 'cambio en fichero 2'
Ahora el historial de tu proyecto diverge. Has creado una rama y saltado a ella, has trabajado sobre ella; has vuelto a la rama original, y has trabajado también sobre ella. Los cambios realizados en ambas sesiones de trabajo están aislados en ramas independientes: puedes saltar libremente de una a otra según estimes oportuno. Y todo ello simplemente con tres comandos: git branch, git checkout y git commit.
Ramas Remotas
Las ramas remotas son referencias al estado de las ramas en tus repositorios remotos. Son ramas locales que no puedes mover; se mueven automáticamente cuando estableces comunicaciones en la red. Las ramas remotas funcionan como marcadores, para recordarte en qué estado se encontraban tus repositorios remotos la última vez que conectaste con ellos. Suelen referenciarse como (remoto)/(rama).
Publicar
Cuando quieres compartir una rama con el resto del mundo, debes llevarla (push) a un remoto donde tengas permisos de escritura. Tus ramas locales no se sincronizan automáticamente con los remotos en los que escribes, sino que tienes que enviar (push) expresamente las ramas que desees compartir. De esta forma, puedes usar ramas privadas para el trabajo que no deseas compartir, llevando a un remoto tan solo aquellas partes que deseas aportar a los demás.
Traer y Fusionar
A pesar de que el comando git fetch trae todos los cambios que no tienes del servidor, este no modifica tu directorio de trabajo. Simplemente obtendrá los datos y dejará que tú mismo los fusiones. Sin embargo, existe un comando llamado git pull, el cuál básicamente hace git fetch seguido por git merge en la mayoría de los casos. Si tienes una rama de seguimiento configurada como vimos en la última sección, bien sea asignándola explícitamente o creándola mediante los comandos clone o checkout, git pull identificará a qué servidor y rama remota sigue tu rama actual, traerá los datos de dicho servidor e intentará fusionar dicha rama remota.
Normalmente es mejor usar los comandos fetch y merge de manera explícita pues la magia de git pull puede resultar confusa.
Eliminar Ramas Remotas
Imagina que ya has terminado con una rama remota, es decir, tanto tú como tus colaboradores habéis completado una determinada funcionalidad y la habéis incorporado (merge) a la rama master en el remoto (o donde quiera que tengáis la rama de código estable). Puedes borrar la rama remota utilizando la opción –delete de git push.
Claves SSH
Como habréis observado, cada vez que hacemos un git push nos pide el usuario y contraseña. Esto es bastante molesto.
Una forma de evitar esto es mediante un par de claves SSH (una clave privada y una clave pública). Ambas se complementa. La una sin la otra no sirve de nada.
Este método evita que nuestro usuario y contraseña de GitHub se guarde en un archivo de disco. Por tanto es muy seguro. En caso de que alguién haga login en nuestro PC podría acceder a nuestras claves. En dicho caso eliminaríamos el par de claves y volveríamos a crear unas nuevas y nuestro usuario y contraseña de GitHub nunca se verían comprometidos.
Vamos a seguir los siguientes pasos:
1. Generamos un par de claves SSH
Es muy sencillo. Como usuario normal (sin ser root) ejecutamos el comando
ssh-keygen
La salida será algo parecido a esto:
Generating public/private rsa key pair.
Enter file in which to save the key (/home/sanclemente.local/sabela/.ssh/id_rsa):
/home/sanclemente.local/sabela/.ssh/id_rsa already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/sanclemente.local/sabela/.ssh/id_rsa
Your public key has been saved in /home/sanclemente.local/sabela/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:DA2VOPu52mR9ZJX1PzXeKEbXltjNNoSRt0qC+LOA0sM sabela@a26eql00
The key's randomart image is:
+---[RSA 3072]----+
| .o.. .+..|
| oo. o+*+|
| .oo . ..==X|
| .+ . o +.=*|
| o ..S. B ooo|
| . E .o+ + o .|
| . . +.+ . |
| +.. . |
| ... |
+----[SHA256]-----+
Pulsamos Intro a todo. En el caso de que ya exista un par de claves nos preguntará si deseamos sobreescribir (Override (y/n)? ). Si queremos proteger nuestra clave privada podemos poner una contraseña.
Esto nos creará una carpeta ~/.ssh y dentro al menos 2 archivos:
id_rsa
id_rsa.pub
sabela@a26eql00:~/.ssh$ ls
id_rsa id_rsa.pub known_hosts known_hosts.old
El primer archivo corresponde a la clave privada y el segundo a la clave pública.
Copiamos el contenido de la clave pública en un editor de texto. Nos hará falta más adelante.
En vuestro caso, en lugar de sabela@a26eql00 aparecerá otro usuario y pc.
2. Añadimos clave ssh pública a github.
Iniciamos sesión de GitHub y en el menú general (esquina superior derecha) seleccionamos la opción Settings.
Luego, en la parte izquierda, elegimos la opción SSH y GPG keys
A continuación, a la derecha, pulsamos en el botón New SSH key
Luego ponemos un nombre a la clave, por ejemplo pc-casa. Y copiamos el contenido de la clave pública. Finalmente, pulsamos en el botón Add SSH key
La clave anterior puede usarse para cualquiera de nuestros repositorios. Para hacer uso de ella, lo único que necesitamos es la URL en formato SSH de cada repositorio.
3. Asociando nuestro repositorio local mediante SSH
Nuestro repositorio local estaba asociado a origin mediante HTTPS. Debemos dar de baja dicho enlace y crear uno nuevo que haga uso del protocolo SSH.